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>
307 lines
18 KiB
Plaintext
307 lines
18 KiB
Plaintext
Sub-scope 01 (objectstorage) governance + config/** - manifest and probes
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T02:38:00+00:00
|
|
|
|
=== OWNED FILES ===
|
|
848a96fae0288a4f45f277e59519162098de42a6 CLAUDE.md 52
|
|
b009f7e1d21c68ae4cade2311d7aeb4a4daf3118 README.md 72
|
|
ad99e57d20a4be7df7ad4514dec20de86c595acc build.gradle 84
|
|
493644f4567e5fae94bc4d4dd45294957b2e66f1 gradle.lockfile 217
|
|
70535fba48e01ac6814e2afffc0498ddb5c009d6 config/CompiledObjectStorageDestination.java 21
|
|
9ae8d2fe0aa878b95d527455e91e90e663c533c7 config/CompiledObjectStorageProvider.java 53
|
|
69f0037741960803ae9fdd8e02de4e45e38796d0 config/LegacyObjectAdoptionSettings.java 41
|
|
1705a48cc64501e17e38c84a2f9ab51840bade70 config/LegacyObjectStorageActivationGuard.java 60
|
|
9bb2f57920e4ba27bbd357310c7b3467bc3dfbce config/ObjectStorageBindingCompiler.java 276
|
|
d8648f156810a06d9ed4158f9503d1a5312dc721 config/ObjectStorageCapabilityAssembler.java 132
|
|
cd1d0f00b8cb2e1206060fa483910e53fb1270c9 config/ObjectStorageCapabilityConfig.java 164
|
|
346fd88cf5030a6a1efe5a4fc66880ecfc547096 config/ObjectStorageCapabilitySettings.java 49
|
|
eb8e5a2d35b58d1bbda8c9a52e5d97251a8c8658 config/ObjectStorageDestinationSettings.java 46
|
|
8ff739407bc795f04c9ba04ac2c425392a450ddf config/ObjectStorageLegacyMigrationConfig.java 37
|
|
784343e77bf981a8c76f72b27ebec0272d5c17b2 config/ObjectStorageMaintenanceCapabilityConfig.java 10
|
|
da0692f4cc012c5ba9f161d5c8cb23c27f730a8e config/ObjectStorageProviderContribution.java 28
|
|
58ea89e50d3918f14faeae29a5cfa38ee479dacb config/ObjectStorageProviderSettings.java 13
|
|
41a53fae2eaec8f6fc965447f3428c947e4afde2 config/ObjectStorageScanMaintenanceConfig.java 20
|
|
2c0559b1d5e051771648f91c5885625d41ab1340 config/RoutingObjectDirectGrantAdapter.java 10
|
|
57723eb8d8f3fd8e81f90fee380540c089b9277c config/RoutingObjectMutationAdapter.java 523
|
|
24ed0f52e84a38abbfc797e42f05a00625eea0de config/RoutingObjectReadAdapter.java 147
|
|
85aaa9a830aeea0452c32e96c08efbda553f9fb9 config/RoutingObjectScanMaintenanceAdapter.java 147
|
|
192666945aceeef3e34cd16fb911f9cd2a5bf54b config/SelectedObjectStorageProviderFactory.java 98
|
|
8474c92e55cb29df477bd0698526be48ec51f163 test/config/ObjectStorageBindingCompilerTest.java 167
|
|
11f1d10fac90b601d6e6cf3858305701787fc610 test/config/ObjectStorageCapabilityConfigTest.java 480
|
|
4effeb8c4acb65b6a19e9f0328634db76d35eb63 test/config/ObjectStorageLegacyMigrationConfigTest.java 66
|
|
b455e7675c64ab00bf1a7c40db14b0a9c06b2d06 test/config/ObjectStorageScanMaintenanceConfigTest.java 56
|
|
5db09bc83df4af94c03719828e29ba7b7be0557e test/config/RoutingObjectStorageAdapterTest.java 154
|
|
|
|
=== 8.1 reachability: how the configs load and what they gate ===
|
|
$ git ls-files 'adapter/outbound/objectstorage' | grep -i 'META-INF\|imports\|spring.factories'
|
|
exit=1
|
|
|
|
$ git grep -rn 'ObjectStorageCapabilityConfig\|ObjectStorageScanMaintenanceConfig\|ObjectStorageMaintenanceCapabilityConfig\|ObjectStorageLegacyMigrationConfig' -- . | grep -v "^adapter/outbound/objectstorage/" | head
|
|
exit=0
|
|
|
|
$ grep -rn 'objectstorage' app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/*.java 2>/dev/null | head
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java:76: "dev.caskeleton.adapter.outbound.objectstorage",
|
|
exit=0
|
|
|
|
$ git grep -rn 'app.object-storage' -- sample-portfolio/src/main/resources app-bootstrap/src/main/resources 2>/dev/null | head -20
|
|
exit=0
|
|
|
|
=== 8.2 the four claims README makes about composition ===
|
|
--- (a) no direct-grant port is registered
|
|
$ grep -n 'class RoutingObjectDirectGrantAdapter' -A 4 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectDirectGrantAdapter.java
|
|
7:public final class RoutingObjectDirectGrantAdapter {
|
|
8-
|
|
9- private RoutingObjectDirectGrantAdapter() {}
|
|
10-}
|
|
exit=0
|
|
|
|
$ git grep -rn 'RoutingObjectDirectGrantAdapter' -- 'adapter/outbound/objectstorage/src' | grep -v 'RoutingObjectDirectGrantAdapter.java'
|
|
exit=1
|
|
|
|
--- (b) scanner and privileged purge remain separate and empty
|
|
$ cat adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageMaintenanceCapabilityConfig.java
|
|
package dev.caskeleton.adapter.outbound.objectstorage.config;
|
|
|
|
import org.springframework.context.annotation.Configuration;
|
|
|
|
/**
|
|
* Separate privileged composition boundary. Batch B intentionally contributes no purge or scanner
|
|
* maintenance bean.
|
|
*/
|
|
@Configuration(proxyBeanMethods = false)
|
|
public class ObjectStorageMaintenanceCapabilityConfig {}
|
|
exit=0
|
|
|
|
$ grep -n '@Bean' -A 6 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageScanMaintenanceConfig.java
|
|
11: @Bean
|
|
12- @ConditionalOnProperty(
|
|
13- prefix = "app.object-storage.scan-maintenance",
|
|
14- name = "enabled",
|
|
15- havingValue = "true")
|
|
16- public RoutingObjectScanMaintenanceAdapter routingObjectScanMaintenanceAdapter(
|
|
17- ObjectStorageCapabilityAssembler.AssembledCapability capability) {
|
|
exit=0
|
|
|
|
--- (c) filesystem-local-dev rejected under prod/production
|
|
$ grep -n 'filesystem-local-dev is rejected\|productionProfileActive' -B 3 -A 6 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageBindingCompiler.java
|
|
122- if (compiled.put(providerId, provider) != null) {
|
|
123- throw invalid();
|
|
124- }
|
|
125: if ("filesystem-local-dev".equals(providerType) && productionProfileActive()) {
|
|
126: throw new IllegalArgumentException("filesystem-local-dev is rejected in production");
|
|
127- }
|
|
128- }
|
|
129- return Map.copyOf(compiled);
|
|
130- }
|
|
131-
|
|
132- private Map<String, CompiledObjectStorageDestination> compileDestinations(
|
|
--
|
|
234- }
|
|
235- }
|
|
236-
|
|
237: private boolean productionProfileActive() {
|
|
238- return activeProfiles.stream()
|
|
239- .map(profile -> profile.toLowerCase(Locale.ROOT))
|
|
240- .anyMatch(profile -> profile.equals("prod") || profile.equals("production"));
|
|
241- }
|
|
242-
|
|
243- private static String canonicalId(String label, String value) {
|
|
exit=0
|
|
|
|
--- (d) mixing old alias with canonical settings fails startup without echoing values
|
|
$ grep -n 'cannot be combined' -B 8 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/LegacyObjectStorageActivationGuard.java
|
|
14- private static final String CANONICAL_PREFIX = "app.object-storage.";
|
|
15-
|
|
16- @Override
|
|
17- public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
|
18- boolean legacy = hasPrefix(context, LEGACY_PREFIX);
|
|
19- boolean canonical = hasPrefix(context, CANONICAL_PREFIX);
|
|
20- if (legacy && canonical) {
|
|
21- throw new IllegalStateException(
|
|
22: "legacy and canonical object-storage configuration cannot be combined");
|
|
exit=0
|
|
|
|
=== 8.2b compile-before-construct: is that ordering real? ===
|
|
$ grep -n 'compiler.compile(settings)' -A 6 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageCapabilityAssembler.java
|
|
25: ObjectStorageBindingCompiler.CompiledBindings bindings = compiler.compile(settings);
|
|
26- if (bindings.destinations().isEmpty()) {
|
|
27- throw new IllegalArgumentException("disabled capability cannot be assembled");
|
|
28- }
|
|
29- Map<String, ObjectStorageProviderContribution> byType = new LinkedHashMap<>();
|
|
30- for (ObjectStorageProviderContribution contribution : contributions) {
|
|
31- for (String providerType : contribution.providerTypes()) {
|
|
exit=0
|
|
|
|
$ grep -n 'contribution.create(provider)' -B 6 -A 8 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageCapabilityAssembler.java
|
|
42- CompiledObjectStorageProvider provider =
|
|
43- Objects.requireNonNull(bindings.providers().get(destination.providerId()));
|
|
44- ObjectStorageProviderContribution contribution = byType.get(provider.providerType());
|
|
45- if (contribution == null) {
|
|
46- throw new IllegalArgumentException("selected provider contribution is unavailable");
|
|
47- }
|
|
48: SelectedObjectStorageProviderFactory factory = contribution.create(provider);
|
|
49- constructed.add(factory);
|
|
50- selected.put(provider.providerId(), factory);
|
|
51- }
|
|
52- return new AssembledCapability(bindings, selected);
|
|
53- } catch (RuntimeException exception) {
|
|
54- for (int index = constructed.size() - 1; index >= 0; index--) {
|
|
55- constructed.get(index).close();
|
|
56- }
|
|
exit=0
|
|
|
|
$ grep -n 'must not resolve credentials' -B 3 -A 3 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageProviderContribution.java
|
|
6-/**
|
|
7- * Side-effect-free descriptor plus an explicitly selected provider construction seam.
|
|
8- *
|
|
9: * <p>{@link #describe} must not resolve credentials, create files, clients, threads, or schedulers.
|
|
10- * {@link #create} owns cleanup of every partial allocation before it throws; after a successful
|
|
11- * return the assembler owns the returned lifecycle exactly once.
|
|
12- */
|
|
exit=0
|
|
|
|
=== 8.3 duplicate mechanism: two legacy activation paths ===
|
|
$ grep -rn 'LEGACY_PREFIX\|app.object-storage.legacy' adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/*.java | head -12
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/LegacyObjectAdoptionSettings.java:9:@ConfigurationProperties(prefix = "app.object-storage.legacy-adoption")
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/LegacyObjectStorageActivationGuard.java:13: private static final String LEGACY_PREFIX = "ca-skeleton.objectstorage.";
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/LegacyObjectStorageActivationGuard.java:18: boolean legacy = hasPrefix(context, LEGACY_PREFIX);
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/LegacyObjectStorageActivationGuard.java:41: if (LEGACY_PREFIX.equals(prefix)) {
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageCapabilityConfig.java:90: prefix = "app.object-storage.legacy",
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageCapabilityConfig.java:115: prefix = "app.object-storage.legacy",
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageLegacyMigrationConfig.java:22: prefix = "app.object-storage.legacy-adoption",
|
|
exit=0
|
|
|
|
$ git grep -rn 'LegacyObjectStorageActivationGuard' -- 'adapter/outbound/objectstorage/src' | grep -v 'LegacyObjectStorageActivationGuard.java'
|
|
adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/ObjectStorageConfig.java:30: dev.caskeleton.adapter.outbound.objectstorage.config.LegacyObjectStorageActivationGuard.class)
|
|
exit=0
|
|
|
|
=== 8.4 the readiness registry the build declares as a test input ===
|
|
$ grep -n 'objectStorageReadinessRegistry' -A 6 adapter/outbound/objectstorage/build.gradle
|
|
45:def objectStorageReadinessRegistry = rootProject.projectDir.parentFile.toPath()
|
|
46- .resolve('docs/registries/object-storage-readiness.yaml').toFile()
|
|
47-tasks.named('test') {
|
|
48: inputs.file(objectStorageReadinessRegistry)
|
|
49- .withPathSensitivity(PathSensitivity.RELATIVE)
|
|
50: systemProperty 'objectstorage.readiness.registry', objectStorageReadinessRegistry.absolutePath
|
|
51-}
|
|
52-
|
|
53-def objectStorageMinioContractQualification = registerStrictQualificationTest(
|
|
54- name: 'objectStorageMinioContractTest',
|
|
55- sourceSet: sourceSets.objectStorageMinioContractTest,
|
|
56- requiredClasses: [
|
|
exit=0
|
|
|
|
$ ls -l ../docs/registries/object-storage-readiness.yaml 2>&1
|
|
-rw-rw-r-- 1 ubuntu ubuntu 4319 Aug 1 23:21 ../docs/registries/object-storage-readiness.yaml
|
|
exit=0
|
|
|
|
$ git grep -rln 'object-storage-readiness' -- 'adapter/outbound/objectstorage/src' ../docs | head
|
|
../docs/superpowers/plans/2026-07-28-objectstorage-production-capability.md
|
|
exit=0
|
|
|
|
=== 8.4b the unguarded split in the read router ===
|
|
$ grep -n 'canonicalText().split' -B 3 -A 3 adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectReadAdapter.java
|
|
100- }
|
|
101-
|
|
102- private Optional<LoadedReference> load(ObjectReference reference) {
|
|
103: String routeToken = reference.canonicalText().split("\\.", -1)[1];
|
|
104- ObjectStorageCapabilityAssembler.DestinationRuntime runtime = capability.route(routeToken);
|
|
105- ObjectControlStore control = runtime.providerFactory().controlStore();
|
|
106- ObjectReferencePointerRecord pointer =
|
|
exit=0
|
|
|
|
$ git grep -rn 'canonicalText' -- application-core/src/main/java/dev/caskeleton/application/objectstorage/model/ObjectReference.java | head
|
|
exit=0
|
|
|
|
|
|
=== 8.2c the production-profile deny check: which names does it recognise? ===
|
|
$ grep -rn 'prod\|production' adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageBindingCompiler.java | grep -i profile
|
|
125: if ("filesystem-local-dev".equals(providerType) && productionProfileActive()) {
|
|
237: private boolean productionProfileActive() {
|
|
240: .anyMatch(profile -> profile.equals("prod") || profile.equals("production"));
|
|
exit=0
|
|
|
|
--- how other leaves in this repo detect a production profile
|
|
$ git grep -rn 'equals("prod")\|equalsIgnoreCase("prod")\|"production"' -- '*/src/main/java' | grep -iE 'profile' | head -12
|
|
exit=0
|
|
|
|
--- profile names the repo actually ships
|
|
$ ls -1 app-bootstrap/src/main/resources/ sample-portfolio/src/main/resources/ 2>/dev/null | grep -i 'application-' | head -20
|
|
application-dev.yml
|
|
application-local.yml
|
|
application-prod.yml
|
|
exit=0
|
|
|
|
$ git grep -rn 'spring.profiles.active' -- . 2>/dev/null | grep -v Binary | head -10
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/logging/ResolvedProfileLoggingContextListener.java:15: * because Logback initialises before {@code spring.profiles.active} resolves and binding to that
|
|
app-bootstrap/src/main/resources/logback-spring.xml:9: Reads the environment variable, not spring.profiles.active.
|
|
app-bootstrap/src/main/resources/logback-spring.xml:12: the environment stamps log lines with the stale one. Binding to spring.profiles.active was
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverRuntimeAssemblyTest.java:148: .withPropertyValues("spring.profiles.active=prod")
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/autoconfigure/httpclient/UnsafeStartupConfigurationTest.java:46: "spring.profiles.active=prod", "app.httpclient.clients[0].tls.trust-all=true")
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/autoconfigure/httpclient/UnsafeStartupConfigurationTest.java:58: "spring.profiles.active=prod",
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/autoconfigure/httpclient/UnsafeStartupConfigurationTest.java:79: "spring.profiles.active=prod", "app.httpclient.clients[0].transport=SIMPLE")
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/StructuredLogFieldContractTest.java:79: .doesNotContain("source=\"spring.profiles.active\"")
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/logging/LogProfileDriftCharacterizationTest.java:20: * {@code spring.profiles.active} yields an empty string: Logback initialises before that property
|
|
app-bootstrap/src/test/java/dev/caskeleton/bootstrap/runtime/OperationalContractRuntimeTest.java:73: "spring.profiles.active=test",
|
|
exit=0
|
|
|
|
=== 8.4c the readiness registry: rows and the test that reads it ===
|
|
$ ls -l ../docs/registries/object-storage-readiness.yaml
|
|
-rw-rw-r-- 1 ubuntu ubuntu 4319 Aug 1 23:21 ../docs/registries/object-storage-readiness.yaml
|
|
exit=0
|
|
|
|
$ head -30 ../docs/registries/object-storage-readiness.yaml
|
|
# Repository owner test: dev.caskeleton.bootstrap.contract.ContractRegistrySchemaGovernanceTest
|
|
# Owner Gradle path: :app-bootstrap:test
|
|
# Semantic owner test: dev.caskeleton.adapter.outbound.objectstorage.readiness.ObjectStorageReadinessRegistryTest
|
|
# Semantic owner Gradle path: :adapter:outbound:objectstorage:test
|
|
schema_version: 1
|
|
claims:
|
|
- card_id: object-storage-managed-upload-single
|
|
provider_type: filesystem-local-dev
|
|
provider_version: jdk-21
|
|
destination_profile: local-managed-integrity
|
|
claimed_level: R1
|
|
evidence_revision: batch-b-local-r1
|
|
evidence_expires_on: ""
|
|
required_tasks:
|
|
- ":adapter:outbound:objectstorage:check"
|
|
limitations:
|
|
- single-process control CAS only; no multi-node linearizability
|
|
- fsync and atomic move tests do not prove power-loss durability
|
|
- local development provider is forbidden in production profiles
|
|
|
|
- card_id: object-storage-managed-upload-multipart
|
|
provider_type: filesystem-local-dev
|
|
provider_version: jdk-21
|
|
destination_profile: local-unimplemented
|
|
claimed_level: R0
|
|
evidence_revision: batch-b-contract-r0
|
|
evidence_expires_on: ""
|
|
required_tasks:
|
|
- ":application-core:check"
|
|
limitations:
|
|
exit=0
|
|
|
|
$ git grep -rln 'object-storage-readiness' -- 'adapter/outbound/objectstorage/src'
|
|
exit=1
|
|
|
|
=== 8.4d ObjectReference canonical text shape (the unguarded split) ===
|
|
$ find . -name 'ObjectReference.java' -path '*objectstorage*' | head
|
|
./application-core/src/main/java/dev/caskeleton/application/objectstorage/identity/ObjectReference.java
|
|
exit=0
|
|
|
|
$ grep -n 'canonicalText\|record ObjectReference\|matches\|split' $(find . -name 'ObjectReference.java' -path '*objectstorage*' | head -1) | head -20
|
|
8: private final String canonicalText;
|
|
10: private ObjectReference(String canonicalText) {
|
|
11: this.canonicalText = ObjectIdentitySupport.requireRouted(canonicalText, "osr1");
|
|
14: public static ObjectReference parse(String canonicalText) {
|
|
15: return new ObjectReference(canonicalText);
|
|
18: public String canonicalText() {
|
|
19: return canonicalText;
|
|
23: return ObjectIdentitySupport.redactedLogToken(canonicalText);
|
|
33: return other instanceof ObjectReference that && canonicalText.equals(that.canonicalText);
|
|
38: return Objects.hash(canonicalText);
|
|
exit=0
|
|
|