Repository navigation
module: bind execute to its plan (M4a): plan, target and execution digests, and a derive-again gate - #43
Merged
Conversation
added 2 commits
November 8, 2026 13:28
…gests, and a derive-again gate Execute ran whatever plan_id, config, binding and operations the request carried: with the same plan_id a changed config, resource_id, host, base_path, key_id, revision, or a swapped (M3-valid) delete all executed, and the same request ran any number of times. The old plan_hash also left out schema_version, encoding, the provider operations and the target, hashed raw JSON bytes, and joined its fields with no separator. plan now takes a target and returns plan_digest, target_digest and execution_digest: sha256 over canonical JSON (cic-canonical-json/1) envelopes with their own domain tags, no string concatenation, an absent field and an empty one the same null. execution_digest binds plan and target and is the value an approval will name. plan_id is retired, with no alias. Plan's lifecycle decides what the target must carry: no observed state and no resource id is a create; observed state needs a well-formed resource id; the other combinations are refused (the cic-module-oracle-cloud#31 guard stays). Execute derives the plan again from desired, observed and the binding and requires the same operation and provider operations in the same order, the three digests, and config equal to desired as a JSON value, all before the first sign. The result echoes execution_digest. The expectations for the digests are language-neutral vectors made by an independent implementation. This shows a request is internally consistent. It does not show approval, authority or single use (M4b, the Relay): two tests pin that a self-consistent request is not approved and that the same one can run again. Existing Execute tests now build their requests through the real Plan; the step-output bytes test runs the steps directly. --- [signing-metadata] key = cic-my-sign-key signature = vault:v1:MEQCIBdcnJ/2KyY5S7fLMOOG9p5Ce5hLaCI7TdW5Z2DylRU3AiAv/aYJVHMA/ks3sAeyB7rvoYAqzY6IeEqjtrd0C22PxA== hash-algorithm = sha256 digest = aFAtqS2oAdhBiPEVcrY0QiRVQxpPnwRcO3WT8Zauq5A= [certificate] -----BEGIN CERTIFICATE----- MIICBjCCAaygAwIBAgIUSnRMR6RPnEbg296XWPOqq/u5PCwwCgYIKoZIzj0EAwIw QzELMAkGA1UEBhMCSFUxGTAXBgNVBAoMEENlbnRyYWxJbmZyYUNvcmUxGTAXBgNV BAMMEENJQyBEZXZlbG9wZXIgQ0EwHhcNMjYwMzIwMTMyMjU5WhcNMjYxMjMxMTMy MjU5WjBFMQswCQYDVQQGEwJIVTEZMBcGA1UECgwQQ2VudHJhbEluZnJhQ29yZTEb MBkGA1UEAwwSR2Fib3IgWm9sdGFuIFNpbmtvMFkwEwYHKoZIzj0CAQYIKoZIzj0D AQcDQgAEIG2CVmTfmLB9pLLclj7YmP2eedAjklpy4LGrU2ijoiy6Xqpuybv7OgJe i+ez31s65NEV8+X/ByeX1cstR988z6N8MHowCQYDVR0TBAIwADAdBgNVHQ4EFgQU yZN6AIX/TNnIJ9GwAa/NRN3ujHAwHwYDVR0jBBgwFoAUXn6CHYzPUqU4JVP8g+OS WeDYjhcwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF BQcDBDAKBggqhkjOPQQDAgNIADBFAiEA+bFzXRoJ4PCQbhAAtpkcMjt0vNj5rEW0 lOMBGDNyaWkCIB1vmM7PcZzv/c9bIrxF5kqv6QXomouhByUfeNUTbpKW -----END CERTIFICATE-----
…, review F1) Plan's new create branch (no observed state, no resource id) derived a plan from any desired config, so a config without the kind's required fields, with a field no caller may set, or with a gross type error still produced a consistent plan that Execute would run. The branch now applies the contract check Validate already does (required, known, JSON kind) before it plans, and Execute, which derives the plan again, refuses the same configs. A refusal names desired.data.<field> (validation for a missing required field, schema for an unknown field or a wrong type) and makes no sign or actuate call; with several problems the first by field path is named, so the answer does not depend on map order. An update is not held to the required rule. Subnet and instance create cases in the derivable-plan test now carry their required fields. --- [signing-metadata] key = cic-my-sign-key signature = vault:v1:MEYCIQC5h/pYGSTsJxkvsB+JDIM8hH+sqGo0nnl9shrKhRDEjAIhAJ44OObJDZciLoUn7eZFjAbDinbwyUekjfsHMlnVyxs6 hash-algorithm = sha256 digest = SwC6QhyfouXD/Yo6wj6dL6sBd3+jQqGZzM+FCkK3III= [certificate] -----BEGIN CERTIFICATE----- MIICBjCCAaygAwIBAgIUSnRMR6RPnEbg296XWPOqq/u5PCwwCgYIKoZIzj0EAwIw QzELMAkGA1UEBhMCSFUxGTAXBgNVBAoMEENlbnRyYWxJbmZyYUNvcmUxGTAXBgNV BAMMEENJQyBEZXZlbG9wZXIgQ0EwHhcNMjYwMzIwMTMyMjU5WhcNMjYxMjMxMTMy MjU5WjBFMQswCQYDVQQGEwJIVTEZMBcGA1UECgwQQ2VudHJhbEluZnJhQ29yZTEb MBkGA1UEAwwSR2Fib3IgWm9sdGFuIFNpbmtvMFkwEwYHKoZIzj0CAQYIKoZIzj0D AQcDQgAEIG2CVmTfmLB9pLLclj7YmP2eedAjklpy4LGrU2ijoiy6Xqpuybv7OgJe i+ez31s65NEV8+X/ByeX1cstR988z6N8MHowCQYDVR0TBAIwADAdBgNVHQ4EFgQU yZN6AIX/TNnIJ9GwAa/NRN3ujHAwHwYDVR0jBBgwFoAUXn6CHYzPUqU4JVP8g+OS WeDYjhcwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF BQcDBDAKBggqhkjOPQQDAgNIADBFAiEA+bFzXRoJ4PCQbhAAtpkcMjt0vNj5rEW0 lOMBGDNyaWkCIB1vmM7PcZzv/c9bIrxF5kqv6QXomouhByUfeNUTbpKW -----END CERTIFICATE-----
sinkog
marked this pull request as ready for review
November 8, 2026 12:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
M4a:
executenow runs only a plan it can derive again from the desired and observed state it was made from, on the target it was made for. Before this,executeran whateverplan_id, config, binding and operations the request carried: measured ondevelwith the sameplan_id, a changed config,resource_id,host,base_path,key_idorrevision, a swapped M3-validDeleteVcn, or a reversed step order all executed, and one approved request ran any number of times. The oldplanHashalso left outschema_version,encoding, the provider operations and the target, hashed raw JSON bytes, and joined its fields with no separator.plan_digest,target_digest,execution_digest): sha256 over canonical JSON (cic-canonical-json/1) envelopes, each with its own domain tag, no string concatenation, an absent field and an empty one bothnull.plan_digest: kind, the kind's contract digest, fulldesiredandobserved(canonicaldataplus every schema field),operation, orderedprovider_operations.reasonandnotesare not in it (explanation only; a documented, deliberate property).target_digest:host,base_path,key_id,resource_id,revision.execution_digest: binds the two; it is the value an approval will name (M4b).plan_idis retired, no alias:planstops emitting it andexecuterefuses a request that carries one (even empty or null). A survey of the locally cloned CentralInfraCore repositories and cic-factory found no consumer of the module'splan_id(only a length check in this module's own test); repos not cloned here and other environments were not checked.plantakes atargetand is lifecycle-aware: no observed state and no resource id is a create plan; observed state needs a well-formed resource id; the other two combinations are refused (the Plan() silently mis-plans a DeleteVcn against a nonexistent resource when observed is empty #31 guard stays).hostandkey_idare required; every field is bounded.executegate, before the first sign: after M1 and M3: noplan_id; the three digests,desiredandobservedpresent; the plan derived again must have the sameoperationand the same provider operations in the same order; the three digests must match;config.datamust be the same JSON value asdesired.data. Every refusal isvalidationwith the dimension infield_pathand zero sign / zero actuate; the result echoesexecution_digest.module/testdata/plan_digest_vectors.json, 16 plan / 11 target / 4 execution) generated by an independent implementation (no Go); spec indocs/design/specs/plan-digest.md.What this does NOT show
A consistent request is internally consistent, nothing more: not approved, not authorised, not single-use. A caller who can change every input and every digest builds another consistent request. Approval, authority, the key's tenant, replay and concurrency are M4b (the Relay). Two tests pin this so nobody reads more into M4a:
TestM4aDoesNotClaimApproval(a request the caller invented, unapproved by anyone, runs) andTestM4aDoesNotPreventReplay(the same request runs three times). Both run on the mocked host seam only; M4b turns them around.Evidence
Planproduced (a field, or the request text), not the helper that builds good ones: ~30 mismatches (config, desired, observed, schema_version/encoding, operation, ops appended/removed/swapped/replaced incl. the M3-valid delete, resource_id, host, base_path, key_id, revision, missing/garbage/swapped digests, another plan's digests, a valid plan paired with another valid target) plus four hand-edited request texts; a bad step in a two-step plan runs no step. Each:validation, expectedfield_path, 0 sign / 0 actuate.1/1.0/10e-1) gives oneplan_digest, and a request that spells it differently from the plan still runs.go vet -tags manual_real_oci); the four real-WASM tests PASS against a fresh guest.operation/ count / per-step compare off, each of the three digest compares off, config equality off,plan_idban off, contract digest constant, revision / encoding dropped from the envelopes, rawdatainstead of canonical, operations sorted before digesting, "derive" from the plan's own operations, resource-id check off, lifecycle branch off, execution tag = target tag.develbefore this work (all the dimension changes above executed, replay 2x/5x), and the gate-off mutant fails the new gate tests; the new tests reference new symbols, so they cannot be compiled against the old tree.Review F1 (a create must come from a config the contract accepts) - decided
Plan(create)takes on intent conformance. Before it plans a create (no observed state, no resource id) it applies the contract checkvalidatealready does: required fields present, no field a caller cannot set, no gross JSON-kind error.execute, which derives the plan again, refuses the same configs. A refusal namesdesired.data.<field>(validationfor a missing required field,schemafor an unknown field or wrong type) with zero sign / zero actuate; with several problems the first by field path is named, so the answer does not depend on map order. The check is the coarse onevalidatedoes, not a JSON Schema engine. An update is not held to the "required" rule. Tests:plan_create_conformance_test.go(8Plancases, 3 hand-edited requests plus an edit of the request text, controls, determinism); RED on the previousprovider.go: all 12 negative cases fail; 5 compiling mutants all fail a test.Behaviour changes to review
plannow plans a create (no observed state and no resource id). It was needed becauseexecuteonly runs a derivable plan and creation used to go through a hand-builtexecute.plancannot produce (a loneDeleteVcn,AddVcnCidr) is no longer runnable throughexecute. A delete isdestroy's job.executeoutput gainsexecution_digest; the step results are unchanged (tested: the output minus the digest equals the steps run directly).Executecall sites now build requests through the realPlan(m4a_helpers_test.go); the step-output bytes test runsexecuteStepsdirectly; the "every registry operation through Execute" test is now a registry-check unit test plus a derivable-plan integration test; one module: Execute runs only registry operations with the registry's method and path (M3) #40 F1 test (create-named step, plain path, junk id) was removed because the M4a target check now refuses that request before M1 alone could be observed.manual_real_oci_test.goExecute harness was rewritten (Observe -> Plan -> Execute;OCI_EXEC_METHOD/OCI_EXEC_PATHgone). It compiles under its tag; it was not run against real OCI.Limits / not in this PR
module.wasmis not rebuilt or re-signed; the committed artifact predates this source, as before. No production GO.🤖 Generated with Claude Code