anvil-api 0.5.3

Rust gRPC API types for Anvil object storage
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
syntax = "proto3";

package anvil.v1;

import "google/protobuf/timestamp.proto";

// The public Anvil 0.5 network contract. Peer and storage-coordination services
// remain private. Ordinary object calls are independent. Only InvokeProgram
// requests atomic multi-path visibility; indexes are asynchronous derived views
// over the same visible object state.
service ObjectService {
  // The complete put header is unary so an upload stream can contain only
  // payload chunks. Its typed operation selects Put, PutIfAbsent,
  // PutIfVersion, or PutImmutable. The returned opaque token binds the
  // authenticated caller, complete header, selected operation, and expiry.
  // This contract does not require StartPut to create durable object state or
  // hold a path lock. An unavailable durability is rejected here, before the
  // client uploads any bytes; it is never silently downgraded.
  rpc StartPut(PutHeader) returns (PutToken);

  // Every request in this client stream carries the UPLOAD-phase token returned
  // by exactly one StartPut call. Closing the stream seals the staged bytes but
  // does not publish an object. The returned READY-phase token is the only
  // token PutEnd accepts. A zero-byte object is one request containing the
  // token and an empty chunk; an empty request stream is invalid.
  rpc Put(stream PutRequest) returns (PutToken);

  // The sole publication point for a sealed streamed upload. An UPLOAD token
  // cannot call PutEnd and a READY token cannot start Put. PutEnd revalidates
  // caller identity, current authorization, operation policy and final CAS.
  rpc PutEnd(PutToken) returns (MutationReceipt);

  // Publish the next tombstone version, including when the path never existed.
  rpc Delete(DeleteRequest) returns (MutationReceipt);
  // Publish the next tombstone only when the current live or tombstone head has
  // exactly the supplied version.
  rpc DeleteIfVersion(DeleteIfVersionRequest) returns (MutationReceipt);
  // Permanently removes one exact retained version from a version-enabled
  // bucket. A missing version is an idempotent successful no-op. Removing a
  // non-current version leaves the head unchanged. Removing the current live
  // version publishes a fresh monotonic tombstone; older retained versions
  // never become current again. The current tombstone is the path's CAS/ABA
  // fence and cannot be removed.
  rpc DeleteVersion(DeleteVersionRequest) returns (DeleteVersionResponse);
  rpc HeadObject(HeadObjectRequest) returns (ObjectHead);
  // Returns one stateless page of current live paths in strict UTF-8 byte
  // order. This is a read-committed view of one page, not a snapshot held
  // across pages. Passing the last returned path as start_after resumes after
  // it. The implementation scans the current-head keyspace directly; it does
  // not require or maintain a side index.
  rpc ListObjects(ListObjectsRequest) returns (ListObjectsResponse);
  rpc GetObject(GetObjectRequest) returns (stream ObjectChunk);
  // Streams retained metadata for one exact path in ascending version order.
  // Payload bytes are never included and the server pages storage internally.
  // This is a bounded-memory read-committed stream, not a snapshot of the
  // entire version history.
  rpc ListObjectVersions(ListObjectVersionsRequest) returns (stream ObjectVersion);
  rpc BulkWrite(BulkWriteRequest) returns (BulkWriteResponse);
  rpc BatchGet(BatchGetRequest) returns (BatchGetResponse);
  rpc WatchPrefix(WatchPrefixRequest) returns (stream WatchMessage);
  rpc SetBucketPolicy(SetBucketPolicyRequest) returns (BucketPolicy);
  rpc InvokeProgram(InvokeProgramRequest) returns (InvokeProgramResponse);
}

// Realm-scoped Zanzibar authorization. Schemas are immutable tenant-owned
// values; bindings and tuple sets are the mutable authorization state.
service AuthzService {
  rpc PutSchema(PutSchemaRequest) returns (PutSchemaResponse);
  rpc BindSchema(BindSchemaRequest) returns (BindSchemaResponse);
  rpc GetBinding(GetBindingRequest) returns (GetBindingResponse);
  rpc GetSchema(GetSchemaRequest) returns (GetSchemaResponse);
  rpc MutateTuples(MutateTuplesRequest) returns (MutateTuplesResponse);
  rpc ReadTuples(ReadTuplesRequest) returns (ReadTuplesResponse);
  rpc CheckPermission(CheckPermissionRequest) returns (CheckPermissionResponse);
  rpc CheckPermissions(CheckPermissionsRequest) returns (CheckPermissionsResponse);
}

// Cluster-wide derived indexes. One weighted-HRW builder publishes immutable
// generations and up to three weighted-HRW query replicas materialize those
// same files through their shared local cache. A query is executed by one
// replica; this service never scatter-queries independent per-node indexes.
service IndexService {
  rpc CreateIndex(CreateIndexRequest) returns (IndexDefinition);
  rpc UpdateIndex(UpdateIndexRequest) returns (IndexDefinition);
  rpc GetIndex(GetIndexRequest) returns (IndexDefinition);
  rpc ListIndexes(ListIndexesRequest) returns (ListIndexesResponse);
  rpc DeleteIndex(DeleteIndexRequest) returns (DeleteIndexResponse);
  rpc QueryIndex(QueryIndexRequest) returns (QueryIndexResponse);
}

// Asynchronous usage aggregates backed by ordinary objects. Bucket accounting
// uses an empty path_prefix; finer-grained accounting is enabled explicitly for
// one canonical path prefix. Configuration and query remain Zanzibar checked.
service AccountingService {
  rpc EnableAccounting(EnableAccountingRequest) returns (AccountingDefinition);
  rpc DisableAccounting(DisableAccountingRequest) returns (DisableAccountingResponse);
  rpc GetAccounting(GetAccountingRequest) returns (AccountingSnapshot);
}

// Credential exchange is the only unauthenticated service. Long-lived client
// credentials are verified against durable Anvil state and exchanged for a
// short-lived bearer token. The deployment must protect this service with TLS
// termination because the request contains a long-lived secret. Every other
// public service is protected.
service CredentialService {
  rpc ExchangeClientCredentials(ExchangeClientCredentialsRequest) returns (AccessToken);
}

// Small, typed management operations for Anvil's protected system realm.
// These RPCs do not bypass Zanzibar: each request is authorized against the
// same protected realm used by object and authorization requests, and each
// resulting ownership or grant is an ordinary system-realm tuple.
service AdministrationService {
  // Prepares one JOINING node identity after system#manage_system
  // authorization. The server writes a mode-0600 one-time bundle and returns
  // its local path for the administrator to copy to the new node and delete.
  rpc PrepareNode(PrepareNodeRequest) returns (PrepareNodeResponse);
  rpc ProvisionTenant(ProvisionTenantRequest) returns (ProvisionTenantResponse);
  rpc CreateApplication(CreateApplicationRequest) returns (ApplicationCredential);
  rpc RotateApplicationCredential(RotateApplicationCredentialRequest) returns (ApplicationCredential);
  rpc DisableApplicationCredential(DisableApplicationCredentialRequest) returns (ApplicationCredentialState);
  rpc CreateBucket(CreateBucketRequest) returns (CreateBucketResponse);
  // Bucket versioning can move only from UNVERSIONED to ENABLED. Disabling a
  // version-enabled bucket is deliberately unsupported.
  rpc SetBucketVersioning(SetBucketVersioningRequest) returns (SetBucketVersioningResponse);
  // Enables or disables unauthenticated GET/list access for one bucket. The
  // server represents anonymous readers as the reserved Zanzibar subject
  // app:_anvil/anonymous; this does not create a manageable application and can
  // never grant write access.
  rpc SetBucketPublicRead(SetBucketPublicReadRequest) returns (SetBucketPublicReadResponse);
  rpc GrantApplicationRole(ApplicationRoleRequest) returns (ApplicationRoleResponse);
  rpc RevokeApplicationRole(ApplicationRoleRequest) returns (ApplicationRoleResponse);
}

message PrepareNodeRequest {
  // Stable Snowflake/Raft node identity in 1..=1023. Removed IDs are never
  // reusable within the cluster.
  uint32 node_id = 1;
  // Connectable host:port advertised on the mandatory-mTLS peer network.
  string peer_address = 2;
  // Positive capacity ratio in millionths; 1000000 means 1.0.
  uint32 storage_weight_millionths = 3;
}

message PrepareNodeResponse {
  // Server-local mode-0600 file to copy to the configured joining node and
  // delete after the copy. Private key or capability bytes are never returned
  // in this protobuf response.
  string join_bundle_path = 1;
  bytes cluster_id = 2;
  uint64 node_id = 3;
  bytes peer_spki_sha256 = 4;
}

message ObjectAddress {
  string tenant = 1;
  string bucket = 2;
  string path = 3;
}

enum IndexKind {
  INDEX_KIND_UNSPECIFIED = 0;
  INDEX_KIND_PATH = 1;
  INDEX_KIND_METADATA_FILTER = 2;
  INDEX_KIND_TYPED_JSON = 3;
  INDEX_KIND_FULL_TEXT = 4;
  INDEX_KIND_VECTOR = 5;
  INDEX_KIND_HYBRID = 6;
  reserved 7;
  INDEX_KIND_GIT_SOURCE = 8;
  INDEX_KIND_TENSOR = 9;
}

message IndexField {
  string name = 1;
  string json_pointer = 2;
}

message PathIndexSpec {}

message MetadataFilterIndexSpec {
  // Names from Anvil's fixed object-head projection: path, version,
  // content_type, content_length, content_hash, and committed_at_unix_millis.
  // Arbitrary user metadata is deliberately not part of the 0.5.2 object API.
  repeated string fields = 1;
}

message TypedJsonIndexSpec {
  repeated IndexField fields = 1;
}

message FullTextField {
  string name = 1;
  string json_pointer = 2;
}

message FullTextIndexSpec {
  repeated FullTextField fields = 1;
}

enum VectorMetric {
  VECTOR_METRIC_COSINE = 0;
  VECTOR_METRIC_DOT = 1;
  VECTOR_METRIC_EUCLIDEAN = 2;
}

message VectorIndexSpec {
  string json_pointer = 1;
  uint32 dimensions = 2;
  VectorMetric metric = 3;
  bool normalize = 4;
}

message HybridIndexSpec {
  FullTextIndexSpec full_text = 1;
  VectorIndexSpec vector = 2;
  // Zero values select equal weighting.
  float full_text_weight = 3;
  float vector_weight = 4;
}

message GitSourceIndexSpec {
  string repository_id = 1;
}

// One exact model tensor-name projection. The definition fixes the model;
// queries select one tensor within it.
message TensorIndexSpec {
  string model_id = 1;
}

message IndexSpecification {
  reserved 7;
  oneof specification {
    PathIndexSpec path = 1;
    MetadataFilterIndexSpec metadata_filter = 2;
    TypedJsonIndexSpec typed_json = 3;
    FullTextIndexSpec full_text = 4;
    VectorIndexSpec vector = 5;
    HybridIndexSpec hybrid = 6;
    GitSourceIndexSpec git_source = 8;
    TensorIndexSpec tensor = 9;
  }
}

// Index names are immutable bucket-local identifiers. Mutable display labels
// belong in application data. Placement uses the server-allocated index_id.
message IndexDefinition {
  uint64 index_id = 1;
  string bucket = 2;
  string name = 3;
  // Segment-aware object path prefix. Empty selects the complete bucket.
  string path_prefix = 4;
  // Empty accepts every content type.
  string content_type = 5;
  IndexKind kind = 6;
  IndexSpecification specification = 7;
  uint64 version = 8;
}

message CreateIndexRequest {
  string bucket = 1;
  string name = 2;
  string path_prefix = 3;
  string content_type = 4;
  IndexSpecification specification = 5;
  string command_id = 6;
}

message UpdateIndexRequest {
  string bucket = 1;
  string name = 2;
  uint64 expected_version = 3;
  string path_prefix = 4;
  string content_type = 5;
  IndexSpecification specification = 6;
  string command_id = 7;
}

message GetIndexRequest {
  string bucket = 1;
  string name = 2;
}

message ListIndexesRequest {
  string bucket = 1;
  optional string start_after_name = 2;
  // Zero selects 100. The maximum page size is 1000; continuation has no
  // arbitrary total-result ceiling.
  uint32 limit = 3;
}

message ListIndexesResponse {
  repeated IndexDefinition indexes = 1;
  bool has_more = 2;
}

message DeleteIndexRequest {
  string bucket = 1;
  string name = 2;
  uint64 expected_version = 3;
  string command_id = 4;
}

message DeleteIndexResponse {
  bool deleted = 1;
}

enum IndexPredicateOperator {
  INDEX_PREDICATE_OPERATOR_UNSPECIFIED = 0;
  INDEX_PREDICATE_OPERATOR_EQUAL = 1;
  INDEX_PREDICATE_OPERATOR_IN = 2;
  INDEX_PREDICATE_OPERATOR_PREFIX = 3;
  INDEX_PREDICATE_OPERATOR_LESS_THAN = 4;
  INDEX_PREDICATE_OPERATOR_LESS_THAN_OR_EQUAL = 5;
  INDEX_PREDICATE_OPERATOR_GREATER_THAN = 6;
  INDEX_PREDICATE_OPERATOR_GREATER_THAN_OR_EQUAL = 7;
  INDEX_PREDICATE_OPERATOR_EXISTS = 8;
}

message IndexPredicate {
  string field = 1;
  IndexPredicateOperator operator = 2;
  // Each value is one canonical JSON scalar. IN accepts multiple values;
  // EXISTS accepts none; every other operator accepts exactly one.
  repeated bytes values_json = 3;
}

enum IndexOrderDirection {
  INDEX_ORDER_DIRECTION_ASCENDING = 0;
  INDEX_ORDER_DIRECTION_DESCENDING = 1;
}

message IndexOrder {
  string field = 1;
  IndexOrderDirection direction = 2;
}

message PathIndexQuery {
  string prefix = 1;
  optional string start_after = 2;
}

message MetadataFilterIndexQuery {
  repeated IndexPredicate predicates = 1;
}

message TypedJsonIndexQuery {
  repeated IndexPredicate predicates = 1;
  repeated IndexOrder order = 2;
}

message FullTextIndexQuery {
  string text = 1;
  bool phrase = 2;
}

message VectorIndexQuery {
  repeated float values = 1;
}

message HybridIndexQuery {
  string text = 1;
  repeated float vector = 2;
}

message GitSourceIndexQuery {
  string commit_id = 1;
  string tree_path = 2;
  bool prefix = 3;
}

message TensorIndexQuery {
  string tensor_name = 1;
}

message IndexQuery {
  reserved 7;
  oneof query {
    PathIndexQuery path = 1;
    MetadataFilterIndexQuery metadata_filter = 2;
    TypedJsonIndexQuery typed_json = 3;
    FullTextIndexQuery full_text = 4;
    VectorIndexQuery vector = 5;
    HybridIndexQuery hybrid = 6;
    GitSourceIndexQuery git_source = 8;
    TensorIndexQuery tensor = 9;
  }
}

message QueryIndexRequest {
  string bucket = 1;
  string index_name = 2;
  IndexQuery query = 3;
  // Zero selects 100. The maximum is 1000.
  uint32 limit = 4;
  bytes page_token = 5;
}

message IndexQueryHit {
  ObjectAddress address = 1;
  uint64 object_version = 2;
  optional float score = 3;
  // Kind-specific canonical JSON fields explicitly retained by the index.
  bytes fields_json = 4;
}

message IndexSourceFreshness {
  uint64 node_id = 1;
  bytes source_epoch = 2;
  // First source offset not represented by this generation.
  uint64 indexed_next_offset = 3;
  // Latest tail observed by the query replica's background source monitor.
  optional uint64 observed_tail = 4;
  uint64 lag_hint = 5;
}

// Freshness is evidence, not an admission rule. A query returns the available
// generation and this structure; it never fails solely because indexing lags.
message IndexFreshness {
  uint64 generation = 1;
  google.protobuf.Timestamp published_at = 2;
  repeated IndexSourceFreshness sources = 3;
  bool initial_build_complete = 4;
  bool rebuilding = 5;
  uint64 authorization_revision = 6;
  uint64 placement_term = 7;
  uint64 placement_index = 8;
  uint64 index_id = 9;
  uint64 definition_version = 10;
  // Whole-index cardinalities are deliberately not public because exact-hit
  // Zanzibar filtering must not leak counts from unauthorized objects.
  reserved 11, 12;
}

message QueryIndexResponse {
  repeated IndexQueryHit hits = 1;
  bytes next_page_token = 2;
  IndexFreshness freshness = 3;
}

message EnableAccountingRequest {
  string bucket = 1;
  // Empty selects the whole bucket. A non-empty value selects the exact path
  // and its slash-delimited children.
  string path_prefix = 2;
  string command_id = 3;
}

message DisableAccountingRequest {
  string bucket = 1;
  string path_prefix = 2;
  uint64 expected_version = 3;
  string command_id = 4;
}

message GetAccountingRequest {
  string bucket = 1;
  string path_prefix = 2;
}

message AccountingDefinition {
  string storage_tenant = 1;
  string bucket = 2;
  string path_prefix = 3;
  uint64 accounting_id = 4;
  uint64 version = 5;
}

message DisableAccountingResponse {
  bool disabled = 1;
  uint64 tombstone_version = 2;
  bool replayed = 3;
}

message AccountingSourceCheckpoint {
  uint64 node_id = 1;
  bytes source_epoch = 2;
  // Highest source-local journal offset included in the aggregate.
  uint64 through_offset = 3;
}

message AccountingFreshness {
  google.protobuf.Timestamp refreshed_at = 1;
  repeated AccountingSourceCheckpoint sources = 2;
  // False only when retained source history was unavailable and the worker
  // had to establish a new current-state baseline.
  bool complete = 3;
}

message AccountingSnapshot {
  AccountingDefinition definition = 1;
  uint64 logical_stored_bytes = 2;
  uint64 object_count = 3;
  uint64 accepted_inbound_bytes = 4;
  uint64 served_outbound_bytes = 5;
  AccountingFreshness freshness = 6;
}

// LOCAL is the default for speed: the ingress acknowledges after its durable
// local write while normal placement continues. REPLICATED waits for the fixed
// 2+1 payload layout (or the corresponding mutable-record quorum) before
// acknowledging. A cluster that cannot satisfy the requested class returns
// DURABILITY_UNAVAILABLE without publishing. Unknown values are invalid.
enum Durability {
  DURABILITY_LOCAL = 0;
  DURABILITY_REPLICATED = 1;
}

// UNVERSIONED is the meaningful zero/default: replacing or deleting a head
// does not retain the predecessor for later reads. ENABLED retains versions
// and is a one-way bucket capability.
enum ObjectVersioning {
  OBJECT_VERSIONING_UNVERSIONED = 0;
  OBJECT_VERSIONING_ENABLED = 1;
}

// Complete metadata and operation for one addressed upload. A header never
// contains payload bytes, a caller-supplied content hash, or a storage
// reference. Exactly one operation must be selected.
message PutHeader {
  ObjectAddress address = 1;
  // Empty means unspecified; otherwise at most 512 UTF-8 bytes.
  string content_type = 2;
  string command_id = 3;
  Durability durability = 4;
  oneof operation {
    PutOperation put = 5;
    PutIfAbsentOperation put_if_absent = 6;
    PutIfVersionOperation put_if_version = 7;
    PutImmutableOperation put_immutable = 8;
  }
}

// Unconditional ordinary put: publish the next version of a MUTABLE path.
message PutOperation {}

// Create on a MUTABLE path only when no live value exists.
message PutIfAbsentOperation {}

// Publish on a MUTABLE path only when its current live or tombstone head has
// exactly expected_version. The condition is checked when the completed upload
// is published; StartPut does not reserve or lock the path while bytes arrive.
message PutIfVersionOperation {
  uint64 expected_version = 1;
}

// Create on an IMMUTABLE path. Identical existing content is an idempotent
// replay; different existing content fails. This operation never changes path
// policy.
message PutImmutableOperation {}

// Opaque upload capability returned by StartPut. Clients must not parse
// `value`. A token is bound to its authenticated caller and one protocol phase.
// StartPut returns an UPLOAD token; Put returns a fresh READY token for PutEnd.
message PutToken {
  bytes value = 1;
  google.protobuf.Timestamp expires_at = 2;
}

// Protobuf cannot make a field unskippable on a hostile wire, so the server
// rejects a missing/empty token. Unlike the old frame union, however, this
// message has no header variant a normal client can send out of order. Every
// request in one stream must carry the same token.
message PutRequest {
  PutToken token = 1;
  bytes chunk = 2;
}

message DeleteRequest {
  ObjectAddress address = 1;
  string command_id = 2;
  Durability durability = 3;
}

message DeleteIfVersionRequest {
  ObjectAddress address = 1;
  string command_id = 2;
  Durability durability = 3;
  uint64 expected_version = 4;
}

message DeleteVersionRequest {
  ObjectAddress address = 1;
  uint64 version = 2;
  Durability durability = 3;
}

message DeleteVersionResponse {
  bool deleted = 1;
  // Present only when the target was the current live payload. Anvil removes
  // that payload and publishes this fresh tombstone as the new head. Removing
  // a non-current version never changes the head.
  optional uint64 replacement_tombstone_version = 2;
}

message MutationReceipt {
  string command_id = 1;
  uint64 version = 2;
  bool deleted = 3;
  bool replayed = 4;
  // The command ID is guaranteed replayable through this instant. Once the
  // bounded receipt expires, the caller must reconcile against object state
  // before retrying an operation whose duplicate execution would matter.
  google.protobuf.Timestamp replay_guarantee_expires_at = 5;
}

message HeadObjectRequest {
  ObjectAddress address = 1;
}

message ListObjectsRequest {
  string tenant = 1;
  string bucket = 2;
  // Literal UTF-8 prefix, bounded by the maximum object-path byte length.
  // Empty lists the whole bucket; '/' has no special delimiter meaning.
  string prefix = 3;
  // Exclusive full-path cursor. When present it must be a canonical object
  // path. The request is stateless and carries no opaque continuation token.
  optional string start_after = 4;
  // Zero selects the default of 100. The maximum is 1000.
  uint32 limit = 5;
}

message ListObjectsResponse {
  // Current live paths only, in strict ascending UTF-8 byte order. No
  // metadata, payload, common-prefix entries, or tombstones are returned.
  repeated string paths = 1;
  bool has_more = 2;
}

// Exact path state. A tombstone has a version and is not the same as a path
// for which no version has ever existed.
message ObjectHead {
  oneof state {
    PresentObject present = 1;
    DeletedObject deleted = 2;
    NeverExisted never_existed = 3;
  }
}

message PresentObject {
  uint64 version = 1;
  bytes content_hash = 2;
  uint64 content_length = 3;
  string content_type = 4;
}

message DeletedObject {
  uint64 version = 1;
}

message NeverExisted {}

message GetObjectRequest {
  ObjectAddress address = 1;
  // Absence selects the current head. Presence selects that exact retained
  // version and is valid only when bucket versioning is ENABLED.
  optional uint64 version = 2;
}

// A GetObject stream begins with exactly one head. Bytes follow only for a
// PresentObject state.
message ObjectChunk {
  oneof value {
    ObjectHead head = 1;
    bytes bytes = 2;
  }
}

message ListObjectVersionsRequest {
  ObjectAddress address = 1;
}

// Retained metadata for one live value or tombstone. NeverExisted is not a
// version and therefore cannot occur in this stream.
message ObjectVersion {
  oneof state {
    PresentObject present = 1;
    DeletedObject deleted = 2;
  }
}

// Bulk operations are independent. The server enforces finite item and byte
// limits; one failed operation does not roll back another successful one.
message BulkWriteRequest {
  repeated BulkOperation operations = 1;
}

// Bulk puts carry their bounded payload inline because there is one enclosing
// unary request. They use the same canonical operation identity and semantics
// as the corresponding StartPut plus Put plus PutEnd sequence.
message BulkPutRequest {
  ObjectAddress address = 1;
  bytes bytes = 2;
  // Empty means unspecified; otherwise at most 512 UTF-8 bytes.
  string content_type = 3;
  string command_id = 4;
  Durability durability = 5;
}

message BulkPutIfVersionRequest {
  ObjectAddress address = 1;
  bytes bytes = 2;
  // Empty means unspecified; otherwise at most 512 UTF-8 bytes.
  string content_type = 3;
  string command_id = 4;
  Durability durability = 5;
  uint64 expected_version = 6;
}

message BulkOperation {
  oneof operation {
    BulkPutRequest put = 1;
    BulkPutRequest put_if_absent = 2;
    BulkPutIfVersionRequest put_if_version = 3;
    BulkPutRequest put_immutable = 4;
    DeleteRequest delete = 5;
    DeleteIfVersionRequest delete_if_version = 6;
  }
}

message BulkWriteResponse {
  repeated BulkOutcome outcomes = 1;
}

message BulkOutcome {
  uint32 index = 1;
  oneof outcome {
    MutationReceipt receipt = 2;
    MutationFailure failure = 3;
  }
}

message MutationFailure {
  MutationFailureCode code = 1;
  string message = 2;
  optional uint64 current_version = 3;
}

enum MutationFailureCode {
  MUTATION_FAILURE_CODE_UNSPECIFIED = 0;
  MUTATION_FAILURE_CODE_CONDITION_FAILED = 1;
  MUTATION_FAILURE_CODE_IMMUTABLE = 2;
  MUTATION_FAILURE_CODE_IDEMPOTENCY_INPUT_MISMATCH = 3;
  MUTATION_FAILURE_CODE_INVALID = 4;
  MUTATION_FAILURE_CODE_INTERNAL = 5;
  MUTATION_FAILURE_CODE_PROGRAM_CONCURRENCY_VIOLATION = 6;
  MUTATION_FAILURE_CODE_DURABILITY_UNAVAILABLE = 7;
  MUTATION_FAILURE_CODE_RESOURCE_LIMIT = 8;
  MUTATION_FAILURE_CODE_AUTHORIZATION_DENIED = 9;
  MUTATION_FAILURE_CODE_IMMUTABLE_POLICY_REQUIRED = 10;
}

// Batch reads are independent and bounded by server item and byte limits.
message BatchGetRequest {
  repeated GetObjectRequest objects = 1;
}

message BatchGetResponse {
  repeated BatchGetOutcome outcomes = 1;
}

message BatchGetOutcome {
  uint32 index = 1;
  ObjectAddress address = 2;
  oneof outcome {
    BatchGetObject object = 3;
    ReadFailure failure = 4;
  }
}

message BatchGetObject {
  ObjectHead head = 1;
  // Empty for DeletedObject and NeverExisted. A present empty object is
  // distinguished by its PresentObject head.
  bytes bytes = 2;
}

message ReadFailure {
  ReadFailureCode code = 1;
  string message = 2;
}

enum ReadFailureCode {
  READ_FAILURE_CODE_UNSPECIFIED = 0;
  READ_FAILURE_CODE_INVALID = 1;
  READ_FAILURE_CODE_AUTHORIZATION_DENIED = 2;
  READ_FAILURE_CODE_VERSION_NOT_FOUND = 3;
  READ_FAILURE_CODE_RESOURCE_LIMIT = 4;
  READ_FAILURE_CODE_DATA_LOSS = 5;
  READ_FAILURE_CODE_INTERNAL = 6;
  READ_FAILURE_CODE_VERSIONING_DISABLED = 7;
}

// WatchPrefix is an unordered, at-least-once invalidation feed. Duplicates are
// legal and rapid changes to one path may be coalesced; intermediate versions
// are not promised. It carries no payload bytes and is not change-data-capture
// or an audit log. A consumer must reread each path until it observes at least
// minimum_path_version.
message WatchPrefixRequest {
  // `path` is a canonical path-segment prefix. Empty selects the whole bucket.
  ObjectAddress prefix = 1;
  oneof start {
    WatchNow now = 2;
    WatchRetainedBeginning retained_beginning = 3;
    bytes resume_token = 4;
  }
}

message WatchNow {}
message WatchRetainedBeginning {}

message WatchMessage {
  oneof message {
    WatchInvalidation invalidation = 1;
    WatchCheckpoint checkpoint = 2;
  }
}

message WatchInvalidation {
  ObjectAddress address = 1;
  uint64 minimum_path_version = 2;
  WatchStateHint state_hint = 3;
}

enum WatchStateHint {
  WATCH_STATE_HINT_UNSPECIFIED = 0;
  WATCH_STATE_HINT_PRESENT = 1;
  WATCH_STATE_HINT_DELETED = 2;
}

// Persist this opaque token only after every preceding invalidation has been
// durably applied. Reconnecting from it may legally redeliver invalidations.
message WatchCheckpoint {
  bytes resume_token = 1;
}

message SetBucketPolicyRequest {
  string tenant = 1;
  string bucket = 2;
  BucketPolicy policy = 3;
}

// Prefixes are bucket-relative. Anything below an immutable prefix is
// write-once; anything below a program-only prefix is writable only through a
// pinned atomic program. Overlap validation belongs to the server.
message BucketPolicy {
  repeated string immutable_path_prefixes = 1;
  repeated string program_only_path_prefixes = 2;
}

// This is the only public operation in this service with atomic multi-path
// visibility. A program definition is an ordinary immutable object at the
// bucket-relative path `_anvil/programs/{name}@{version}`. It is
// written through StartPut with PutImmutableOperation, followed by Put and
// PutEnd, and is governed by the same path authorization as every other
// object. The invocation pins that ordinary object's full address and content
// hash.
message InvokeProgramRequest {
  ObjectAddress program = 1;
  string invocation_id = 2;
  // Exactly the 32-byte BLAKE3 hash returned by HeadObject for the pinned
  // immutable program definition.
  bytes program_hash = 3;
  // UTF-8 JSON object containing arguments, inputs, blobs and bindings. Anvil
  // validates and canonicalizes it before deriving the invocation fingerprint.
  bytes input_json = 4;
  Durability durability = 5;
}

message ProgramPathReceipt {
  ObjectAddress address = 1;
  uint64 version = 2;
  bool deleted = 3;
}

message InvokeProgramResponse {
  string invocation_id = 1;
  ObjectAddress program = 2;
  bytes program_hash = 3;
  uint64 executor_nomination_log_index = 4;
  uint64 commit_log_index = 5;
  repeated ProgramPathReceipt path_receipts = 6;
  // Canonical JSON object containing the pinned program's named outputs.
  bytes output_json = 7;
  bool replayed = 8;
  google.protobuf.Timestamp replay_guarantee_expires_at = 9;
}

message ExchangeClientCredentialsRequest {
  string client_id = 1;
  string client_secret = 2;
}

message AccessToken {
  string access_token = 1;
  string token_type = 2;
  uint64 expires_in_seconds = 3;
}

// Creates one storage tenant, its first owner application and that
// application's credential in one durable metadata write. Only a caller with
// system#manage_system may perform this operation.
message ProvisionTenantRequest {
  // Exact lowercase ASCII DNS label (1..63 bytes). The server rejects rather
  // than normalizes other spellings. `_anvil` is reserved for Anvil's
  // protected system tenant. Once claimed, a name remains bound to its first
  // stable tenant identity even after any future tenant release.
  string storage_tenant = 1;
  string owner_app_id = 2;
  string owner_client_id = 3;
  string owner_client_secret = 4;
}

message ProvisionTenantResponse {
  ApplicationCredential credential = 1;
  uint64 authorization_revision = 2;
  bool replayed = 3;
}

// Application IDs and client IDs are cluster-wide authentication identities;
// each is globally unique. An application has exactly one client credential
// in Anvil 0.5. Supplying the same canonical application, client ID and secret is
// an idempotent replay.
message CreateApplicationRequest {
  string app_id = 1;
  string client_id = 2;
  string client_secret = 3;
}

message ApplicationCredential {
  string storage_tenant = 1;
  string app_id = 2;
  string client_id = 3;
  bool active = 4;
  bool replayed = 5;
}

message RotateApplicationCredentialRequest {
  string app_id = 1;
  string client_id = 2;
  string client_secret = 3;
}

// Stops future exchanges. Access tokens already issued to this application
// remain valid until their stated expiry.
message DisableApplicationCredentialRequest {
  string app_id = 1;
  string client_id = 2;
}

message ApplicationCredentialState {
  string storage_tenant = 1;
  string app_id = 2;
  string client_id = 3;
  bool active = 4;
  bool replayed = 5;
}

// A bucket's durable existence marker and first system-realm owner tuple are
// written in one metadata batch. The caller becomes that owner.
message CreateBucketRequest {
  string bucket = 1;
  ObjectVersioning versioning = 2;
}

message CreateBucketResponse {
  string storage_tenant = 1;
  string bucket = 2;
  uint64 authorization_revision = 3;
  bool replayed = 4;
  ObjectVersioning versioning = 5;
}

// Only ENABLED is accepted. UNVERSIONED is selected at CreateBucket time by
// omission/default and cannot be restored after versioning has been enabled.
message SetBucketVersioningRequest {
  string bucket = 1;
  ObjectVersioning versioning = 2;
}

message SetBucketVersioningResponse {
  string storage_tenant = 1;
  string bucket = 2;
  ObjectVersioning versioning = 3;
  bool changed = 4;
}

message SetBucketPublicReadRequest {
  string bucket = 1;
  bool enabled = 2;
}

message SetBucketPublicReadResponse {
  string storage_tenant = 1;
  string bucket = 2;
  bool enabled = 3;
  uint64 authorization_revision = 4;
  bool replayed = 5;
}

message ApplicationRoleRequest {
  string app_id = 1;
  oneof target {
    SystemApplicationRoleTarget system = 2;
    TenantApplicationRoleTarget tenant = 3;
    BucketApplicationRoleTarget bucket = 4;
  }
}

message SystemApplicationRoleTarget {
  SystemApplicationRole role = 1;
}

enum SystemApplicationRole {
  SYSTEM_APPLICATION_ROLE_UNSPECIFIED = 0;
  SYSTEM_APPLICATION_ROLE_ADMIN = 1;
}

message TenantApplicationRoleTarget {
  TenantApplicationRole role = 1;
}

enum TenantApplicationRole {
  TENANT_APPLICATION_ROLE_UNSPECIFIED = 0;
  TENANT_APPLICATION_ROLE_OWNER = 1;
  TENANT_APPLICATION_ROLE_ADMIN = 2;
  TENANT_APPLICATION_ROLE_READER = 3;
  TENANT_APPLICATION_ROLE_MANAGE_TENANT = 4;
  TENANT_APPLICATION_ROLE_READ_TENANT = 5;
  TENANT_APPLICATION_ROLE_MANAGE_BUCKETS = 6;
  TENANT_APPLICATION_ROLE_MANAGE_AUTHZ = 7;
}

message BucketApplicationRoleTarget {
  string bucket = 1;
  BucketApplicationRole role = 2;
}

enum BucketApplicationRole {
  BUCKET_APPLICATION_ROLE_UNSPECIFIED = 0;
  BUCKET_APPLICATION_ROLE_OWNER = 1;
  BUCKET_APPLICATION_ROLE_ADMIN = 2;
  BUCKET_APPLICATION_ROLE_READER = 3;
  BUCKET_APPLICATION_ROLE_WRITER = 4;
  BUCKET_APPLICATION_ROLE_GET_OBJECT = 5;
  BUCKET_APPLICATION_ROLE_PUT_OBJECT = 6;
  BUCKET_APPLICATION_ROLE_DELETE_OBJECT = 7;
  BUCKET_APPLICATION_ROLE_MANAGE_POLICY = 8;
}

message ApplicationRoleResponse {
  uint64 authorization_revision = 1;
  bool replayed = 2;
}

// The structural boundary for an authorization graph. `default` is the
// conventional application realm. `_anvil/system` is deliberately
// representable here because Anvil uses the same data model internally;
// public calls targeting that reserved scope are rejected by the server. For
// every public call, storage_tenant must equal the authenticated caller's
// immutable storage tenant; spelling another tenant never changes identity.
message AuthzScope {
  string storage_tenant = 1;
  string realm = 2;
}

// A canonical immutable schema identity. The server assigns the revision and
// digest; clients never publish either as metadata.
message SchemaRef {
  string schema_id = 1;
  uint64 schema_revision = 2;
  bytes schema_digest = 3;
}

// Identifiers, namespaces, relations, schema IDs, and realm IDs are limited
// to 256 UTF-8 bytes. Opaque object IDs and exact paths are limited to 4096
// UTF-8 bytes. A realm is carried only by AuthzScope, never in a namespace.
message ObjectRef {
  string namespace = 1;
  oneof id {
    string opaque_id = 2;
    ObjectAddress exact_path = 3;
  }
}

message Userset {
  ObjectRef object = 1;
  string relation = 2;
}

// A tuple subject is either one typed object or one userset in the request's
// realm. The reserved public principal is the object `app:_anvil/public`;
// schemas must still opt into it with PublicSubjectSelector.
message Subject {
  oneof kind {
    ObjectRef object = 1;
    Userset userset = 2;
  }
}

message NamespaceDefinition {
  string name = 1;
  repeated RelationDefinition relations = 2;
}

message RelationDefinition {
  string name = 1;
  oneof kind {
    DirectRelation direct = 2;
    Permission permission = 3;
  }
}

// Direct relations are the only schema members that accept tuples.
message DirectRelation {
  repeated SubjectSelector allowed_subjects = 1;
}

// A permission is a bounded union of its rules and cannot accept tuples.
message Permission {
  repeated PermissionRule rules = 1;
}

message SubjectSelector {
  oneof selector {
    // Any canonical non-public object in this namespace.
    AnyObjectSelector any_object = 1;
    AnyUsersetSelector any_userset = 2;
    Subject exact = 3;
    SameResourceIdSelector same_resource_id = 4;
    PublicSubjectSelector public = 5;
  }
}

message AnyObjectSelector {
  string namespace = 1;
}

message AnyUsersetSelector {
  string namespace = 1;
  string relation = 2;
}

message SameResourceIdSelector {
  string namespace = 1;
}

message PublicSubjectSelector {}

message PermissionRule {
  oneof rule {
    InheritRule inherit = 1;
    TupleToUsersetRule tuple_to_userset = 2;
  }
}

message InheritRule {
  string relation = 1;
}

message TupleToUsersetRule {
  string tuple_relation = 1;
  string target_relation = 2;
}

// A 0.5 schema contains at most 256 namespaces, 256 members per namespace,
// and 256 selectors or rules per member. Publication canonicalizes the body:
// replaying identical content returns the same SchemaRef, while changed
// content creates the next immutable revision for the schema ID.
message PutSchemaRequest {
  string schema_id = 1;
  repeated NamespaceDefinition namespaces = 2;
}

message PutSchemaResponse {
  SchemaRef schema_ref = 1;
  // Tenant-wide authorization revision evaluated or advanced by this call.
  uint64 revision = 2;
  bool replayed = 3;
}

message SchemaBinding {
  AuthzScope scope = 1;
  SchemaRef schema_ref = 2;
  uint64 generation = 3;
}

message BindSchemaRequest {
  AuthzScope scope = 1;
  SchemaRef schema_ref = 2;
  // First binding accepts absence or zero. Rebinding requires the exact
  // current generation.
  optional uint64 expected_binding_generation = 3;
}

message BindSchemaResponse {
  SchemaBinding binding = 1;
  uint64 revision = 2;
}

message GetBindingRequest {
  AuthzScope scope = 1;
}

message GetBindingResponse {
  SchemaBinding binding = 1;
}

// Schema lookup is exact: callers obtain a complete immutable reference from
// PutSchema or GetBinding rather than asking for an implicit latest revision.
message GetSchemaRequest {
  SchemaRef schema_ref = 1;
}

message GetSchemaResponse {
  SchemaRef schema_ref = 1;
  repeated NamespaceDefinition namespaces = 2;
}

message RelationTuple {
  ObjectRef object = 1;
  string relation = 2;
  Subject subject = 3;
}

message TupleMutation {
  oneof operation {
    RelationTuple add = 1;
    RelationTuple remove = 2;
  }
}

// The non-empty batch is one all-or-nothing set mutation in one realm. Add
// and remove are idempotent. The 0.5 hard limit is 1000 mutations and the
// operation ID is limited to 128 UTF-8 bytes. Reusing an operation ID with a
// different canonical input is rejected.
message MutateTuplesRequest {
  AuthzScope scope = 1;
  string operation_id = 2;
  // When present, this is an exact CAS against the tenant-wide authorization
  // revision. A mismatch rejects the complete batch.
  optional uint64 expected_revision = 3;
  repeated TupleMutation mutations = 4;
}

message MutateTuplesResponse {
  // One tenant-wide authorization revision covers the complete batch.
  uint64 revision = 1;
  bool replayed = 2;
  // The operation ID is guaranteed replayable through this instant. Once it
  // expires, the same ID may be treated as a new operation.
  google.protobuf.Timestamp replay_guarantee_expires_at = 3;
}

// Object filters select either one namespace or one exact typed object.
message ObjectFilter {
  oneof selection {
    string namespace = 1;
    ObjectRef exact = 2;
  }
}

// Omitted fields are wildcards. Supplying an exact Subject matches that typed
// object or userset only.
message TupleFilter {
  ObjectFilter object = 1;
  optional string relation = 2;
  Subject subject = 3;
}

message AuthzConsistency {
  // Latest evaluates the current authoritative revision. AtLeast evaluates
  // current state only when it has reached the requested revision. Anvil 0.5
  // retains only current authorization state: Exact(current) succeeds and an
  // older exact revision fails with AUTHZ_REVISION_EXPIRED.
  oneof requirement {
    LatestConsistency latest = 1;
    AtLeastRevision at_least = 2;
    ExactRevision exact = 3;
  }
}

message LatestConsistency {}

message AtLeastRevision {
  uint64 revision = 1;
}

message ExactRevision {
  uint64 revision = 1;
}

// An absent consistency value means latest. page_size zero selects the server
// default of 100; the 0.5 hard maximum is 1000. page_token is opaque and
// limited to 128 KiB. A returned token pins its continuation to the same
// filters and revision; a no-longer-current revision fails rather than moving.
message ReadTuplesRequest {
  AuthzScope scope = 1;
  TupleFilter filter = 2;
  AuthzConsistency consistency = 3;
  uint32 page_size = 4;
  string page_token = 5;
}

message ReadTuplesResponse {
  repeated RelationTuple tuples = 1;
  uint64 revision = 2;
  string next_page_token = 3;
}

message PermissionCheck {
  Subject subject = 1;
  ObjectRef object = 2;
  // May name either a direct relation or a derived permission.
  string relation = 3;
}

message CheckPermissionRequest {
  AuthzScope scope = 1;
  PermissionCheck check = 2;
  AuthzConsistency consistency = 3;
}

message CheckPermissionResponse {
  bool allowed = 1;
  uint64 revision = 2;
}

message PermissionResult {
  bool allowed = 1;
}

// At most 1000 checks. The server pins one authoritative snapshot, so every
// result is evaluated at the single returned revision and retains input order.
message CheckPermissionsRequest {
  AuthzScope scope = 1;
  repeated PermissionCheck checks = 2;
  AuthzConsistency consistency = 3;
}

message CheckPermissionsResponse {
  repeated PermissionResult results = 1;
  uint64 revision = 2;
}