diff --git a/docs/completeness.md b/docs/completeness.md index 23893cb..3e3c2b5 100644 --- a/docs/completeness.md +++ b/docs/completeness.md @@ -1161,7 +1161,9 @@ python3 internal/completeness/generate_tools153.py \ python3 internal/completeness/generate_tools156.py \ /tmp/schema-tools153.pb.gz /tmp/schema-tools156.pb.gz python3 internal/completeness/generate_config157.py \ - /tmp/schema-tools156.pb.gz internal/completeness/schema.pb.gz + /tmp/schema-tools156.pb.gz /tmp/schema-runtime157.pb.gz +python3 internal/completeness/generate_planexecute157.py \ + /tmp/schema-runtime157.pb.gz internal/completeness/schema.pb.gz ``` This authenticates the prior inputs, all 380 records in the 1.4.6 snapshot, @@ -1171,7 +1173,8 @@ exactly six additions over the intermediate 1.4.5 bundle, then only the Finish message over that 1.4.6 bundle, then the subagent state additions, the two 1.4.9 fields and their import, the parent messages field, the config150 and config152 model declarations, the single tools153 field, the single tools156 enum value, -and the runtime157 template-map field and entry. The tests also reproduce +the runtime157 template-map field and entry, and the planexecute157 field and +message. The tests also reproduce the embedded bundle. The [native qualification report](compatibility/agy-1.4.6-compaction.md) records the observed attachment representations and configuration values. @@ -1477,13 +1480,50 @@ and executor metadata. Its entries get the same strict pre-decoding wire checks as the earlier qualified maps; duplicate keys and unknown entry members cannot disappear during decoding. No source-only rule changed. -The current compressed bundle is 138,560 bytes, SHA-256 +The `runtime157` intermediate compressed bundle is 138,560 bytes, SHA-256 `2ce6f4cebef928bca35f94365bf9abaf14067fc9ab841bf6eed2d001b58f25ab`; its decompressed descriptor SHA-256 is `e603b335cff88609a63b131cf92491c8af64f9c402e8282d967b060d61a6336a`. -The final command in the overlay chain above reproduces this bundle: +The final commands in the overlay chain above reproduce the current bundle: ```sh python3 internal/completeness/generate_config157.py \ - /tmp/schema-tools156.pb.gz internal/completeness/schema.pb.gz + /tmp/schema-tools156.pb.gz /tmp/schema-runtime157.pb.gz +python3 internal/completeness/generate_planexecute157.py \ + /tmp/schema-runtime157.pb.gz internal/completeness/schema.pb.gz ``` + +### Plan/execute planner configuration + +The `planexecute157` profile extension retains `CascadePlannerConfig` field 60 +`plan_execute_config` and its `PlanExecuteConfig` message, copied from the +same authenticated agy 1.5.7 closure as `runtime157`. The nested optional +`execution_model` enum uses the existing historical `Model` definition; no +other fields, model values or historical declarations change. + +This setting can occur in both generator and executor metadata. Previously, +either occurrence stopped offline export with `unknown persisted protobuf +fields`. Both now participate in ordinary full/default comparison and remain +in offline JSON, including explicit zero-value presence. No source-only +allowance or new step projection is introduced. The profile changes so saved +attestations must be verified again. + +The generator authenticates the prior uncompressed descriptor bundle with +SHA-256 `e603b335cff88609a63b131cf92491c8af64f9c402e8282d967b060d61a6336a` +and the source closure with SHA-256 +`f3de51c7f2a7317d073cabcea13cd42a7f7f1adf3986c3bf5b3f07035b120d46`. +Maintenance tests reproduce the bundle and require exactly this two-declaration +addition. Synthetic SQLite tests check both metadata locations, absent and +explicit-zero execution models, full/default equality, rejection of changes +and omissions, and lossless offline export. Unknown fields, unsupported enum +values and incorrect wire types still fail closed. + +A private copy of the reported native database exports both persisted steps +as JSON and Markdown after the addition. No native RPC comparison was available +because daemon discovery failed during this investigation; this result proves +offline export, not native full/default RPC qualification for the new setting. + +The resulting compressed bundle is 138,615 bytes, SHA-256 +`a98ccbf848a2ec82dd489484dfdbf9684c171014d366c6a1db8fbb3d41bff0ec`; +its decompressed descriptor SHA-256 is +`f05ed42c8ceb5eea6cb003f354b1fffbe8f4169cdce2a297049849624bde4680`. diff --git a/internal/cli/export_offline_test.go b/internal/cli/export_offline_test.go index bf3c19c..33a3e13 100644 --- a/internal/cli/export_offline_test.go +++ b/internal/cli/export_offline_test.go @@ -17,7 +17,7 @@ import ( "github.com/mjacobs/agy-reader/internal/testfixture" ) -const offlineFixture = `{"cascadeId":"offline-fixture","metadata":{},"steps":[{"type":"CORTEX_STEP_TYPE_USER_INPUT","status":"CORTEX_STEP_STATUS_DONE","metadata":{},"userInput":{"userResponse":"Offline hello"}}]}` +const offlineFixture = `{"cascadeId":"offline-fixture","metadata":{},"generatorMetadata":[{"plannerConfig":{"planExecuteConfig":{"executionModel":1669}}}],"executorMetadatas":[{"cascadeConfig":{"plannerConfig":{"planExecuteConfig":{"executionModel":1669}}}}],"steps":[{"type":"CORTEX_STEP_TYPE_USER_INPUT","status":"CORTEX_STEP_STATUS_DONE","metadata":{},"userInput":{"userResponse":"Offline hello"}}]}` func TestExportOfflineStandalone(t *testing.T) { var requests atomic.Int32 diff --git a/internal/completeness/config157_test.go b/internal/completeness/config157_test.go index ddb4d5d..f275f09 100644 --- a/internal/completeness/config157_test.go +++ b/internal/completeness/config157_test.go @@ -52,10 +52,10 @@ func TestTemplateOverrides157RejectsLossyMapWire(t *testing.T) { } } -func TestRuntime157InvalidatesPreviousAttestations(t *testing.T) { - previous := strings.TrimSuffix(Profile, "-runtime157") +func TestPlanExecute157InvalidatesPreviousAttestations(t *testing.T) { + previous := strings.TrimSuffix(Profile, "-planexecute157") if previous == Profile { - t.Fatal("runtime qualification did not advance profile") + t.Fatal("plan/execute qualification did not advance profile") } snapshot, _ := fixture(t) attestation, result := snapshot.VerifyResponses([]byte(syntheticRaw), []byte(syntheticPresentation)) diff --git a/internal/completeness/generate_planexecute157.py b/internal/completeness/generate_planexecute157.py new file mode 100644 index 0000000..0dccf62 --- /dev/null +++ b/internal/completeness/generate_planexecute157.py @@ -0,0 +1,66 @@ +"""Add authenticated plan/execute configuration without replacing historical definitions.""" +import argparse +import gzip +import hashlib +from pathlib import Path +import tempfile + +from google.protobuf import descriptor_pb2 +import generate_options145 as helpers + +SNAPSHOT = Path(__file__).resolve().parents[2] / "artifacts/proto-snapshots/agy-1.5.7-5df525a2d690/closure.protoset" +SNAPSHOT_HASH = "f3de51c7f2a7317d073cabcea13cd42a7f7f1adf3986c3bf5b3f07035b120d46" +BASE_HASH = "e603b335cff88609a63b131cf92491c8af64f9c402e8282d967b060d61a6336a" + + +def augment(base, snapshot=SNAPSHOT): + data = snapshot.read_bytes() + helpers.require(hashlib.sha256(data).hexdigest() == SNAPSHOT_HASH, "snapshot hash mismatch") + source = descriptor_pb2.FileDescriptorSet.FromString(data) + source.DiscardUnknownFields() + result = descriptor_pb2.FileDescriptorSet() + result.CopyFrom(base) + target, current = (helpers.one(bundle.file, helpers.CORTEX_FILE) for bundle in (result, source)) + helpers.require((target.package, target.syntax) == (current.package, current.syntax), + "source and target file semantics differ") + planner, original = (helpers.one(file.message_type, "CascadePlannerConfig") + for file in (target, current)) + field = helpers.one(original.field, "plan_execute_config") + helpers.require(field.number == 60, "unexpected plan_execute_config field number") + helpers.require(not any(f.name == field.name or f.number == field.number for f in planner.field), + "plan_execute_config already declared") + helpers.require(not any(m.name == "PlanExecuteConfig" for m in target.message_type), + "PlanExecuteConfig already declared") + target.message_type.add().CopyFrom(helpers.one(current.message_type, "PlanExecuteConfig")) + planner.field.add().CopyFrom(field) + helpers.load_pool(result) + return result + + +def generate(previous, output): + helpers.require(output.resolve() not in {previous.resolve(), SNAPSHOT.resolve()}, + "refusing to replace an authenticated input") + data = gzip.decompress(previous.read_bytes()) + helpers.require(hashlib.sha256(data).hexdigest() == BASE_HASH, "base hash mismatch") + result = augment(descriptor_pb2.FileDescriptorSet.FromString(data)) + temporary = None + try: + with tempfile.NamedTemporaryFile(dir=output.parent, prefix=".schema-", delete=False) as stream: + temporary = Path(stream.name) + stream.write(gzip.compress(result.SerializeToString(deterministic=True), mtime=0)) + temporary.chmod(0o644) + temporary.replace(output) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("previous", type=Path) + parser.add_argument("output", type=Path) + args = parser.parse_args() + try: + generate(args.previous, args.output) + except (ValueError, OSError) as error: + parser.exit(1, f"refused: {error}\n") diff --git a/internal/completeness/planexecute157_test.go b/internal/completeness/planexecute157_test.go new file mode 100644 index 0000000..26a0ab7 --- /dev/null +++ b/internal/completeness/planexecute157_test.go @@ -0,0 +1,84 @@ +package completeness + +import ( + "bytes" + "encoding/json" + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/mjacobs/agy-reader/internal/testfixture" +) + +func TestPlanExecute157RetainedAndCompared(t *testing.T) { + for _, config := range []string{`{}`, `{"executionModel":0}`, `{"executionModel":235}`} { + for _, metadata := range []string{ + `"generatorMetadata":[{"plannerConfig":{"planExecuteConfig":%s}}]`, + `"executorMetadatas":[{"cascadeConfig":{"plannerConfig":{"planExecuteConfig":%s}}}]`, + } { + raw := `{"cascadeId":"fixture","metadata":{},` + fmt.Sprintf(metadata, config) + `,"steps":[]}` + path := filepath.Join(t.TempDir(), "source.db") + testfixture.Source(t, path, []byte(raw)) + s, err := ReadSnapshot(t.Context(), path) + if err != nil { + t.Fatal(err) + } + if a, r := s.VerifyResponses([]byte(raw), []byte(raw)); r.State != Verified || len(a.SourceOnly) != 0 { + t.Fatalf("configuration did not compare normally: %+v", r) + } + changes := []string{ + strings.Replace(raw, `"planExecuteConfig":`+config, `"planExecuteConfig":{"executionModel":269}`, 1), + strings.Replace(raw, `{"planExecuteConfig":`+config+`}`, `{}`, 1), + } + if config != `{}` { + changes = append(changes, strings.Replace(raw, config, `{}`, 1)) + } + for _, changed := range changes { + if r := s.CompareFullRaw([]byte(changed)); r.State != Incomplete { + t.Fatalf("changed configuration passed full comparison: %+v", r) + } + if r := s.ComparePresentation([]byte(changed)); r.State != Incomplete { + t.Fatalf("changed configuration passed presentation comparison: %+v", r) + } + } + exported, err := s.OfflineJSON() + if err != nil || !bytes.Contains(exported, []byte(`"planExecuteConfig"`)) { + t.Fatalf("offline export lost configuration: %v", err) + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(exported, &fields); err != nil { + t.Fatal(err) + } + delete(fields, "agyReader") + offline, err := json.Marshal(fields) + if err != nil { + t.Fatal(err) + } + if r := s.CompareFullRaw(offline); r.State != Verified { + t.Fatalf("offline export changed configuration or presence: %+v", r) + } + } + } +} + +func TestPlanExecute157StrictWire(t *testing.T) { + for _, value := range []uint64{0, 235, 1669} { + if _, err := decode("exa.cortex_pb.CascadePlannerConfig", bytesField(60, options145Varint(1, value)...)); err != nil { + t.Fatal(err) + } + } + for name, data := range map[string][]byte{ + "unknown nested field": bytesField(60, options145Varint(99, 1)...), + "unknown model": bytesField(60, options145Varint(1, 999999)...), + "wrong nested wire": bytesField(60, bytesField(1, 'x')...), + "wrong outer wire": options145Varint(60, 1), + "unknown planner field": options145Varint(9999, 1), + } { + t.Run(name, func(t *testing.T) { + if _, err := decode("exa.cortex_pb.CascadePlannerConfig", data); err == nil { + t.Fatal("unsupported configuration accepted") + } + }) + } +} diff --git a/internal/completeness/schema.go b/internal/completeness/schema.go index 28495f4..111ea8e 100644 --- a/internal/completeness/schema.go +++ b/internal/completeness/schema.go @@ -126,8 +126,10 @@ import ( // alternative while preserving exact historical equality when RPC retains them. // runtime157 adds authenticated template overrides and captured task input, // permission, cancellation, zero-iteration cron and extended swarm forms. +// planexecute157 retains the authenticated planner plan/execute configuration, +// including execution-model presence, in both generator and executor metadata. // Every historical declaration and source-only rule remains intact. -const Profile = "sqlite-v1-format0-agy-1.3.4-5b93565c5f04-planner50-permission2-prompt23-tools143-permission1-tools144-skill1-history2-ide225-options145-compaction146-skill146-finish147-replay147-failedread146-subagents148-media149-tools149-sendmessage149-files1411-media1410-parentmsgs1410-paths1410-denied1412-cwd1412-unrun1412-config150-skill150-frontmatter150-bgcancel150-readlines150-config152-tools153-tools154-tools155-images155-tools156-retained156-legacychoices156-runtime157" +const Profile = "sqlite-v1-format0-agy-1.3.4-5b93565c5f04-planner50-permission2-prompt23-tools143-permission1-tools144-skill1-history2-ide225-options145-compaction146-skill146-finish147-replay147-failedread146-subagents148-media149-tools149-sendmessage149-files1411-media1410-parentmsgs1410-paths1410-denied1412-cwd1412-unrun1412-config150-skill150-frontmatter150-bgcancel150-readlines150-config152-tools153-tools154-tools155-images155-tools156-retained156-legacychoices156-runtime157-planexecute157" const maxBytes = 64 << 20 const maxDepth = 100 diff --git a/internal/completeness/schema.pb.gz b/internal/completeness/schema.pb.gz index 93fe0ec..befb137 100644 Binary files a/internal/completeness/schema.pb.gz and b/internal/completeness/schema.pb.gz differ diff --git a/scripts/test_generate_compaction146.py b/scripts/test_generate_compaction146.py index cf3f276..fcd9001 100644 --- a/scripts/test_generate_compaction146.py +++ b/scripts/test_generate_compaction146.py @@ -8,6 +8,8 @@ import sys import tempfile import unittest + +from test_generate_planexecute157 import strip_planexecute from unittest import mock try: @@ -57,6 +59,7 @@ def test_deterministic_exact_overlay_preserves_every_old_declaration(self): # The committed bundle may have been compressed by another zlib version. production = descriptor_pb2.FileDescriptorSet.FromString(gzip.decompress( (REPO / "internal/completeness/schema.pb.gz").read_bytes())) + strip_planexecute(production) # The later overlays have their own exact-preservation tests. tools = generator.one(production.file, "third_party/jetski/cortex/core/tools_pb/tools.proto") tools.message_type.remove(generator.one(tools.message_type, "Finish")) diff --git a/scripts/test_generate_config150.py b/scripts/test_generate_config150.py index 84e1230..85d324a 100644 --- a/scripts/test_generate_config150.py +++ b/scripts/test_generate_config150.py @@ -24,6 +24,7 @@ def test_exact_overlay_and_reproduction(self): import generate_tools153 import generate_tools156 import generate_config157 + import generate_planexecute157 augmented = generate_config152.augment(result) previous = Path(directory) / 'config152.gz' previous.write_bytes(gzip.compress(augmented.SerializeToString(deterministic=True), mtime=0)) @@ -31,7 +32,7 @@ def test_exact_overlay_and_reproduction(self): generate_tools153.generate(previous, intermediate) generate_tools156.generate(intermediate, output) current = descriptor_pb2.FileDescriptorSet.FromString(gzip.decompress(output.read_bytes())) - self.assertEqual(generate_config157.augment(current).SerializeToString(deterministic=True), gzip.decompress( + self.assertEqual(generate_planexecute157.augment(generate_config157.augment(current)).SerializeToString(deterministic=True), gzip.decompress( (REPO / 'internal/completeness/schema.pb.gz').read_bytes())) current_cortex = generator.one(current.file, generator.CORTEX_FILE) spec = generator.one(current_cortex.message_type, 'SubagentSpec') diff --git a/scripts/test_generate_config157.py b/scripts/test_generate_config157.py index cec325b..20b5c28 100644 --- a/scripts/test_generate_config157.py +++ b/scripts/test_generate_config157.py @@ -5,6 +5,8 @@ import tempfile import unittest +from test_generate_planexecute157 import strip_planexecute + from test_generate_config150 import descriptor_pb2, REPO if descriptor_pb2 is not None: @@ -14,8 +16,8 @@ @unittest.skipIf(descriptor_pb2 is None, 'google.protobuf is not installed') class Config157(unittest.TestCase): def production(self): - return descriptor_pb2.FileDescriptorSet.FromString(gzip.decompress( - (REPO / 'internal/completeness/schema.pb.gz').read_bytes())) + return strip_planexecute(descriptor_pb2.FileDescriptorSet.FromString(gzip.decompress( + (REPO / 'internal/completeness/schema.pb.gz').read_bytes()))) def base(self): result = self.production() diff --git a/scripts/test_generate_planexecute157.py b/scripts/test_generate_planexecute157.py new file mode 100644 index 0000000..9981ad3 --- /dev/null +++ b/scripts/test_generate_planexecute157.py @@ -0,0 +1,70 @@ +"""Authenticate the exact plan/execute delta and preserve historical definitions.""" +import gzip +import hashlib +from pathlib import Path +import tempfile +import unittest + +from test_generate_parentmsgs1410 import descriptor_pb2, REPO + +if descriptor_pb2 is not None: + import generate_planexecute157 as generator + + +def strip_planexecute(bundle): + cortex = generator.helpers.one(bundle.file, generator.helpers.CORTEX_FILE) + planner = generator.helpers.one(cortex.message_type, 'CascadePlannerConfig') + planner.field.remove(generator.helpers.one(planner.field, 'plan_execute_config')) + cortex.message_type.remove(generator.helpers.one(cortex.message_type, 'PlanExecuteConfig')) + return bundle + + +@unittest.skipIf(descriptor_pb2 is None, 'google.protobuf is not installed') +class PlanExecute157(unittest.TestCase): + def production(self): + return descriptor_pb2.FileDescriptorSet.FromString(gzip.decompress( + (REPO / 'internal/completeness/schema.pb.gz').read_bytes())) + + def base(self): + result = strip_planexecute(self.production()) + self.assertEqual(hashlib.sha256(result.SerializeToString(deterministic=True)).hexdigest(), generator.BASE_HASH) + return result + + def test_exact_authenticated_delta(self): + base = self.base() + result = generator.augment(base) + self.assertEqual(result, self.production()) + target = generator.helpers.one(result.file, generator.helpers.CORTEX_FILE) + source = descriptor_pb2.FileDescriptorSet.FromString(generator.SNAPSHOT.read_bytes()) + source.DiscardUnknownFields() + original = generator.helpers.one(source.file, generator.helpers.CORTEX_FILE) + planner = generator.helpers.one(target.message_type, 'CascadePlannerConfig') + source_planner = generator.helpers.one(original.message_type, planner.name) + field = generator.helpers.one(planner.field, 'plan_execute_config') + self.assertEqual(field, generator.helpers.one(source_planner.field, field.name)) + message = generator.helpers.one(target.message_type, 'PlanExecuteConfig') + self.assertEqual(message, generator.helpers.one(original.message_type, message.name)) + self.assertEqual(strip_planexecute(result), base) + with self.assertRaisesRegex(ValueError, 'already declared'): + generator.augment(self.production()) + + def test_generation_and_tampering(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + previous, output = root / 'previous.gz', root / 'output.gz' + previous.write_bytes(gzip.compress(self.base().SerializeToString(deterministic=True), compresslevel=1, mtime=42)) + generator.generate(previous, output) + self.assertEqual(gzip.decompress(output.read_bytes()), self.production().SerializeToString(deterministic=True)) + saved = output.read_bytes() + previous.write_bytes(gzip.compress(b'changed')) + with self.assertRaisesRegex(ValueError, 'base hash'): + generator.generate(previous, output) + self.assertEqual(output.read_bytes(), saved) + bad = root / 'bad.protoset' + bad.write_bytes(generator.SNAPSHOT.read_bytes() + b'changed') + with self.assertRaisesRegex(ValueError, 'snapshot hash'): + generator.augment(self.base(), bad) + self.assertEqual(output.read_bytes(), saved) + self.assertFalse(list(root.glob('.schema-*'))) + with self.assertRaisesRegex(ValueError, 'authenticated input'): + generator.generate(generator.SNAPSHOT, generator.SNAPSHOT) diff --git a/scripts/test_generate_tools156.py b/scripts/test_generate_tools156.py index 09ec88b..81d9730 100644 --- a/scripts/test_generate_tools156.py +++ b/scripts/test_generate_tools156.py @@ -7,6 +7,8 @@ import tempfile import unittest +from test_generate_planexecute157 import strip_planexecute + from test_generate_config150 import descriptor_pb2, REPO if descriptor_pb2 is not None: @@ -18,6 +20,7 @@ class Tools156(unittest.TestCase): def production156(self): bundle = descriptor_pb2.FileDescriptorSet.FromString(gzip.decompress( (REPO / 'internal/completeness/schema.pb.gz').read_bytes())) + strip_planexecute(bundle) message = generator.helpers.one(generator.helpers.one(bundle.file, generator.helpers.CORTEX_FILE).message_type, 'PromptSectionCustomizationConfig') message.field.remove(generator.helpers.one(message.field, 'template_overrides')) message.nested_type.remove(generator.helpers.one(message.nested_type, 'TemplateOverridesEntry'))