The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
253 lines
14 KiB
Plaintext
253 lines
14 KiB
Plaintext
Sub-scope 02 (objectstorage) control/** - canonical codec + durable record types
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T02:42:36+00:00
|
|
|
|
=== OWNED FILES ===
|
|
2aad9351e0ff5ed233e9214d3a1f4cb00e08856d control/CanonicalJsonObjectControlRecordCodec.java 898
|
|
181741da87e8e2cb446a12357fb92e253f1797f2 control/CanonicalJsonReader.java 144
|
|
cb68ae97f9f05107a889aa6c6c8ee9c8b902163d control/CanonicalJsonWriter.java 90
|
|
fb3b7ff53be9c5cb4e15f87d0fb4062f3a5fb88f control/ControlRecordSupport.java 37
|
|
66afca559b3bbfe7339c164275be64261063429a control/ObjectControlConflictException.java 11
|
|
afb47e404c79ce53c833758d0e09cff6584c3a4a control/ObjectControlCorruptionException.java 15
|
|
5e557c1346e0b608a64bde312f9f7ea753b1ae5e control/ObjectControlMutation.java 13
|
|
f11454db71aee82a0121da9815085539c6ee1e21 control/ObjectControlRecord.java 42
|
|
0d46ec38359f4b8a67c9894bc7dd84666e78bf86 control/ObjectControlRecordCodec.java 9
|
|
8952f710cdcb1e9ac0dfba38a8c38a59b8a330a9 control/ObjectControlRecordEnvelope.java 45
|
|
00219ff8fa7400ec15dd5342ac625e3450ffa651 control/ObjectControlStore.java 22
|
|
3c5092718ba9b840525734cd8c8c0e1d2265d721 control/ObjectControlVersion.java 18
|
|
e74088f13b773f1e500a7906a5ee0788341f3afc control/ObjectDirectMultipartGrantRecord.java 96
|
|
d7ee355f993afad82dccf31980ceacaf4aafecd8 control/ObjectDirectMultipartSessionRecord.java 198
|
|
2e8b26782800b119eec56a851c4c49fd392d770e control/ObjectDirectSessionRecord.java 32
|
|
33e0de98997a246d2ca4d73027c8d55bf60ceed2 control/ObjectManifestRecord.java 53
|
|
b33587f0c5ee6df490172334130397310cd32fb7 control/ObjectMultipartPartRecord.java 41
|
|
057db5996cc3d1bd04f2df1039c1ad7e7575f6b3 control/ObjectMultipartSessionRecord.java 46
|
|
9d777251edc5e12077f959bb48f9426319060925 control/ObjectOperationRecord.java 210
|
|
cec085f7ba744dbb0f549e50eed0db2c2f874f20 control/ObjectPublicationHandoffRecord.java 188
|
|
a72bfdafe079cacc721a5d7c90fb3fc98a82e303 control/ObjectReferencePointerRecord.java 32
|
|
6c38f41ba9826a83290ae1e068d66b2f5e4363fa control/ObjectReferenceRecord.java 39
|
|
fb3f5735058c2e2b623a2d5923f42242edee4bae control/ObjectStagedObjectRecord.java 180
|
|
c1c1c83a2e5ac08da148e92a047bd8c7ddc8aec3 control/UnsupportedObjectControlSchemaException.java 11
|
|
f8eee5cf1a763bb3b40401481d798691a2207b12 test/control/ObjectControlRecordCodecTest.java 339
|
|
main LOC: 2470 total
|
|
|
|
=== 8.1 reachability: does any control type escape the leaf? ===
|
|
$ git grep -rln 'objectstorage.control' -- . | grep -v "^adapter/outbound/objectstorage/"
|
|
exit=1
|
|
|
|
$ grep -c 'public' adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectControlRecord.java adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectControlStore.java
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectControlRecord.java:1
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectControlStore.java:2
|
|
exit=0
|
|
|
|
=== 8.2 canonical strictness: what the reader and writer refuse ===
|
|
$ grep -n 'printable ASCII only\|character < 0x20 || character > 0x7e' adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonWriter.java adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonReader.java adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ControlRecordSupport.java
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonWriter.java:82: } else if (character < 0x20 || character > 0x7e) {
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonWriter.java:83: throw new IllegalArgumentException("canonical JSON supports printable ASCII only");
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonReader.java:119: } else if (character < 0x20 || character > 0x7e) {
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ControlRecordSupport.java:25: || value.chars().anyMatch(character -> character < 0x20 || character > 0x7e)) {
|
|
exit=0
|
|
|
|
$ grep -n 'private void field' -A 10 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonReader.java
|
|
91: private void field(String expectedName) {
|
|
92- if (first) {
|
|
93- first = false;
|
|
94- } else {
|
|
95- expect(',');
|
|
96- }
|
|
97- if (!quoted().equals(expectedName)) {
|
|
98- throw corrupt();
|
|
99- }
|
|
100- expect(':');
|
|
101- }
|
|
exit=0
|
|
|
|
$ grep -n 'public void end' -A 6 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonReader.java
|
|
84: public void end() {
|
|
85- expect('}');
|
|
86- if (cursor != input.length()) {
|
|
87- throw corrupt();
|
|
88- }
|
|
89- }
|
|
90-
|
|
exit=0
|
|
|
|
$ grep -n 'digitStart == cursor' -B 6 -A 4 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonReader.java
|
|
53- cursor++;
|
|
54- }
|
|
55- int digitStart = cursor;
|
|
56- while (cursor < input.length() && input.charAt(cursor) >= '0' && input.charAt(cursor) <= '9') {
|
|
57- cursor++;
|
|
58- }
|
|
59: if (digitStart == cursor
|
|
60- || (input.charAt(digitStart) == '0' && cursor - digitStart > 1)
|
|
61- || (negative && input.charAt(digitStart) == '0')) {
|
|
62- throw corrupt();
|
|
63- }
|
|
exit=0
|
|
|
|
=== 8.2b envelope: digest + canonical Base64 round trip ===
|
|
$ grep -n 'Base64.getEncoder().encodeToString(payload).equals(base64Payload)' -B 8 -A 6 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonObjectControlRecordCodec.java
|
|
81- }
|
|
82- byte[] payload;
|
|
83- try {
|
|
84- payload = Base64.getDecoder().decode(base64Payload);
|
|
85- } catch (IllegalArgumentException exception) {
|
|
86- throw new ObjectControlCorruptionException(
|
|
87- "control payload is not canonical Base64", exception);
|
|
88- }
|
|
89: if (!Base64.getEncoder().encodeToString(payload).equals(base64Payload)
|
|
90- || !MessageDigest.isEqual(
|
|
91- digest.getBytes(StandardCharsets.US_ASCII),
|
|
92- sha256Hex(payload).getBytes(StandardCharsets.US_ASCII))) {
|
|
93- throw corrupt();
|
|
94- }
|
|
95- ObjectControlRecord record = decodePayload(payload);
|
|
exit=0
|
|
|
|
$ grep -n 'MAXIMUM_ENVELOPE_BYTES\|MAXIMUM_TERMINAL_RECEIPT_BYTES\|MAXIMUM_PART_BYTES\|enforceFamilySize' adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonObjectControlRecordCodec.java
|
|
47: private static final int MAXIMUM_ENVELOPE_BYTES = 64 * 1024;
|
|
48: private static final int MAXIMUM_TERMINAL_RECEIPT_BYTES = 16 * 1024;
|
|
49: private static final int MAXIMUM_PART_BYTES = 4 * 1024;
|
|
55: enforceFamilySize(record, payload.length);
|
|
72: if (encoded == null || encoded.length == 0 || encoded.length > MAXIMUM_ENVELOPE_BYTES) {
|
|
96: enforceFamilySize(record, payload.length);
|
|
107: if (encoded.length > MAXIMUM_ENVELOPE_BYTES) {
|
|
885: private static void enforceFamilySize(ObjectControlRecord record, int payloadLength) {
|
|
888: ? MAXIMUM_PART_BYTES
|
|
889: : record.terminal() ? MAXIMUM_TERMINAL_RECEIPT_BYTES : MAXIMUM_ENVELOPE_BYTES;
|
|
exit=0
|
|
|
|
=== 8.2c the reader decodes bytes leniently; what closes that? ===
|
|
$ grep -n 'new String(bytes, StandardCharsets.UTF_8)' -B 4 -A 3 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonReader.java
|
|
12- public CanonicalJsonReader(byte[] bytes) {
|
|
13- if (bytes == null) {
|
|
14- throw corrupt();
|
|
15- }
|
|
16: input = new String(bytes, StandardCharsets.UTF_8);
|
|
17- expect('{');
|
|
18- }
|
|
19-
|
|
exit=0
|
|
|
|
$ grep -n 'character < 0x20 || character > 0x7e' -B 3 -A 2 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonReader.java
|
|
116- if (character != '"' && character != '\\') {
|
|
117- throw corrupt();
|
|
118- }
|
|
119: } else if (character < 0x20 || character > 0x7e) {
|
|
120- throw corrupt();
|
|
121- }
|
|
exit=0
|
|
|
|
=== 8.3 sealed family + exhaustive switch ===
|
|
$ grep -n 'sealed interface ObjectControlRecord' -A 14 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectControlRecord.java
|
|
4:public sealed interface ObjectControlRecord
|
|
5- permits ObjectDirectSessionRecord,
|
|
6- ObjectDirectMultipartGrantRecord,
|
|
7- ObjectDirectMultipartSessionRecord,
|
|
8- ObjectManifestRecord,
|
|
9- ObjectMultipartPartRecord,
|
|
10- ObjectMultipartSessionRecord,
|
|
11- ObjectOperationRecord,
|
|
12- ObjectPublicationHandoffRecord,
|
|
13- ObjectReferencePointerRecord,
|
|
14- ObjectReferenceRecord,
|
|
15- ObjectStagedObjectRecord {
|
|
16-
|
|
17- int schemaVersion();
|
|
18-
|
|
exit=0
|
|
|
|
$ grep -n 'return switch (record)' -A 14 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonObjectControlRecordCodec.java
|
|
128: return switch (record) {
|
|
129- case ObjectDirectSessionRecord direct -> directSession(writer, direct);
|
|
130- case ObjectDirectMultipartSessionRecord session -> directMultipartSession(writer, session);
|
|
131- case ObjectDirectMultipartGrantRecord grant -> directMultipartGrant(writer, grant);
|
|
132- case ObjectOperationRecord operation -> operation(writer, operation);
|
|
133- case ObjectManifestRecord manifest -> manifest(writer, manifest);
|
|
134- case ObjectReferencePointerRecord pointer -> pointer(writer, pointer);
|
|
135- case ObjectReferenceRecord reference -> reference(writer, reference);
|
|
136- case ObjectStagedObjectRecord staged -> staged(writer, staged);
|
|
137- case ObjectPublicationHandoffRecord handoff -> handoff(writer, handoff);
|
|
138- case ObjectMultipartSessionRecord session -> session(writer, session);
|
|
139- case ObjectMultipartPartRecord part -> part(writer, part);
|
|
140- };
|
|
141- }
|
|
142-
|
|
exit=0
|
|
|
|
$ grep -n 'enum Family' -A 14 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectControlRecord.java | grep -c ','
|
|
10
|
|
exit=0
|
|
|
|
=== 8.3b LIST is deliberately absent ===
|
|
$ grep -n 'LIST is deliberately absent' -B 2 -A 8 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectControlStore.java
|
|
4-import java.util.Optional;
|
|
5-
|
|
6:/** Provider-neutral exact-lookup/create/CAS control storage. LIST is deliberately absent. */
|
|
7-public interface ObjectControlStore {
|
|
8-
|
|
9- Optional<StoredControlRecord> read(String key);
|
|
10-
|
|
11- StoredControlRecord create(String key, ObjectControlRecord record);
|
|
12-
|
|
13- StoredControlRecord compareAndSet(String key, ObjectControlMutation mutation);
|
|
14-
|
|
exit=0
|
|
|
|
$ git grep -rn 'interface ObjectControlStore' -A 8 -- 'adapter/outbound/objectstorage/src/main' | head -12
|
|
fatal: unable to resolve revision: -A
|
|
exit=0
|
|
|
|
=== 8.4 record invariants: do the compact constructors re-derive rather than trust? ===
|
|
$ grep -n 'actualPolicyDigest' -B 6 -A 4 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectOperationRecord.java
|
|
40- requestFingerprint = ControlRecordSupport.hexDigest("requestFingerprint", requestFingerprint);
|
|
41- operationKind = ControlRecordSupport.bounded("operationKind", operationKind, 64);
|
|
42- Objects.requireNonNull(route, "route must be non-null");
|
|
43- Objects.requireNonNull(policySnapshot, "policySnapshot must be non-null");
|
|
44- policySnapshotDigest =
|
|
45- ControlRecordSupport.hexDigest("policySnapshotDigest", policySnapshotDigest);
|
|
46: String actualPolicyDigest =
|
|
47- new CanonicalJsonObjectControlRecordCodec()
|
|
48- .sha256Hex(ObjectPolicySnapshotCodec.encode(policySnapshot));
|
|
49: if (!actualPolicyDigest.equals(policySnapshotDigest)) {
|
|
50- throw new IllegalArgumentException("policySnapshotDigest does not match the snapshot");
|
|
51- }
|
|
52- if (expectedContentIdentity != null
|
|
53- && expectedContentIdentity.exactLength() > policySnapshot.maximumObjectBytes()) {
|
|
exit=0
|
|
|
|
$ grep -n 'pending effect and certainty do not agree' -B 4 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectOperationRecord.java
|
|
58- throw new IllegalArgumentException("stateVersion must be positive");
|
|
59- }
|
|
60- Objects.requireNonNull(effectCertainty, "effectCertainty must be non-null");
|
|
61- if ((pendingEffect == null) != (effectCertainty == ObjectEffectCertainty.NOT_SENT)) {
|
|
62: throw new IllegalArgumentException("pending effect and certainty do not agree");
|
|
exit=0
|
|
|
|
$ grep -n 'active handoff lease is expired at write time\|cannot be released and abort-authorized' -B 4 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectPublicationHandoffRecord.java
|
|
37- }
|
|
38- claimantDigest = ControlRecordSupport.hexDigest("claimantDigest", claimantDigest);
|
|
39- Objects.requireNonNull(leaseExpiresAt, "leaseExpiresAt must be non-null");
|
|
40- if (!released && !abortAuthorized && !leaseExpiresAt.isAfter(updatedAt)) {
|
|
41: throw new IllegalArgumentException("active handoff lease is expired at write time");
|
|
42- }
|
|
43- if (released && abortAuthorized) {
|
|
44: throw new IllegalArgumentException("handoff cannot be released and abort-authorized");
|
|
exit=0
|
|
|
|
$ grep -nE '@Test| void ' adapter/outbound/objectstorage/src/test/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectControlRecordCodecTest.java | head -30
|
|
57: @Test
|
|
58: void everyClosedRecordFamilyRoundTripsByteForByte() {
|
|
79: @Test
|
|
80: void operationGoldenEnvelopeIsFrozen() {
|
|
87: @Test
|
|
88: void everyRecordFamilyHasAFrozenGoldenEnvelopeDigest() {
|
|
104: @Test
|
|
105: void corruptionUnknownOuterFieldsTruncationAndOversizeFailClosed() {
|
|
124: @Test
|
|
125: void duplicateUnknownMissingAndReorderedPayloadFieldsFailClosed() {
|
|
144: @Test
|
|
145: void newerSchemaIsQuarantinedInsteadOfAppearingAbsent() {
|
|
exit=0
|
|
|