diff --git a/.aoci/baseline.json b/.aoci/baseline.json index 28ce5255..499796dc 100644 --- a/.aoci/baseline.json +++ b/.aoci/baseline.json @@ -1,7 +1,7 @@ { "version": 1, "created_at": "2026-08-07T17:44:36Z", - "updated_at": "2026-10-14T19:35:29Z", + "updated_at": "2026-10-16T01:23:36Z", "files": { ".gitattributes": { "role": "index", @@ -137,9 +137,9 @@ }, "aoci.code.txt": { "role": "index", - "sha256": "21fc0c3b60157f4d7f815c5e884b49f91c3b032115ce2ca5452e33e7c156355f", - "size": 208106, - "normalized_sha256": "21fc0c3b60157f4d7f815c5e884b49f91c3b032115ce2ca5452e33e7c156355f" + "sha256": "c9acd01b0e8983898d774c42485c7f283ba3b3271a73267455540655c8f20ae4", + "size": 209819, + "normalized_sha256": "c9acd01b0e8983898d774c42485c7f283ba3b3271a73267455540655c8f20ae4" }, "aoci.meta.txt": { "role": "index", @@ -4631,6 +4631,14 @@ "format_sha256": "70464c1d67a9fc176422396f6b6c886715d59b6aefbe886b5afe0e4d96addd01", "format_kind": "gofmt" }, + "internal/machinecontract/overview_session.go": { + "role": "index", + "sha256": "308b456b09b95d12c9d617a4f630328e9a93452aa1c5acf6253c9104016eff0d", + "size": 412, + "normalized_sha256": "308b456b09b95d12c9d617a4f630328e9a93452aa1c5acf6253c9104016eff0d", + "format_sha256": "308b456b09b95d12c9d617a4f630328e9a93452aa1c5acf6253c9104016eff0d", + "format_kind": "gofmt" + }, "internal/machinecontract/status.go": { "role": "index", "sha256": "dcdf49bdcdf94eac0dc6ceab3691afe7df93b6331583730a57aa71fc07260446", @@ -4737,10 +4745,10 @@ }, "internal/managedstate/state.go": { "role": "index", - "sha256": "f82bd21e8edcba27674d651ea187412d8d1a265c40bcfc5aff22699da8646d06", - "size": 5423, - "normalized_sha256": "f82bd21e8edcba27674d651ea187412d8d1a265c40bcfc5aff22699da8646d06", - "format_sha256": "f82bd21e8edcba27674d651ea187412d8d1a265c40bcfc5aff22699da8646d06", + "sha256": "7460e9daec21d32adf672b6a51c80e3974a1b9f417dc08ce42e54bdeec0c26b7", + "size": 5616, + "normalized_sha256": "7460e9daec21d32adf672b6a51c80e3974a1b9f417dc08ce42e54bdeec0c26b7", + "format_sha256": "7460e9daec21d32adf672b6a51c80e3974a1b9f417dc08ce42e54bdeec0c26b7", "format_kind": "gofmt" }, "internal/managedstate/state_test.go": { @@ -4865,10 +4873,26 @@ }, "internal/mcptools/overview_delivery_v1.go": { "role": "index", - "sha256": "5760476f868969f948d1521d27aaf8941ab34d414a22238b1e83320d27a6f689", - "size": 25015, - "normalized_sha256": "5760476f868969f948d1521d27aaf8941ab34d414a22238b1e83320d27a6f689", - "format_sha256": "5760476f868969f948d1521d27aaf8941ab34d414a22238b1e83320d27a6f689", + "sha256": "7f80654614c0388b1a57e326ce1d2c9acde9e98f0489451806de0385023098ac", + "size": 26513, + "normalized_sha256": "7f80654614c0388b1a57e326ce1d2c9acde9e98f0489451806de0385023098ac", + "format_sha256": "7f80654614c0388b1a57e326ce1d2c9acde9e98f0489451806de0385023098ac", + "format_kind": "gofmt" + }, + "internal/mcptools/overview_diagnostics.go": { + "role": "index", + "sha256": "05819b5cbbb1999b26ba86ddc5790433954c5498ebd0b778300a9315baf25845", + "size": 2203, + "normalized_sha256": "05819b5cbbb1999b26ba86ddc5790433954c5498ebd0b778300a9315baf25845", + "format_sha256": "05819b5cbbb1999b26ba86ddc5790433954c5498ebd0b778300a9315baf25845", + "format_kind": "gofmt" + }, + "internal/mcptools/overview_diagnostics_test.go": { + "role": "observe", + "sha256": "d835e399c99bba624f979d0efce36aafcacd0c4a2bcfe74ce7818c80543afc8d", + "size": 4240, + "normalized_sha256": "d835e399c99bba624f979d0efce36aafcacd0c4a2bcfe74ce7818c80543afc8d", + "format_sha256": "d835e399c99bba624f979d0efce36aafcacd0c4a2bcfe74ce7818c80543afc8d", "format_kind": "gofmt" }, "internal/mcptools/overview_fuzz_test.go": { @@ -4895,6 +4919,30 @@ "format_sha256": "1288eb8eff232dc7bada91274df3f2ba093c3f306a5059816e4f65fb81ef2dbe", "format_kind": "gofmt" }, + "internal/mcptools/overview_session_v2.go": { + "role": "index", + "sha256": "3cbf6393a62d39310d27d2ee5b57016864c494a014cff0132cafa85822503a07", + "size": 10978, + "normalized_sha256": "3cbf6393a62d39310d27d2ee5b57016864c494a014cff0132cafa85822503a07", + "format_sha256": "3cbf6393a62d39310d27d2ee5b57016864c494a014cff0132cafa85822503a07", + "format_kind": "gofmt" + }, + "internal/mcptools/overview_session_v2_test.go": { + "role": "observe", + "sha256": "b3a546d58b1900847a01d08219a6668abdf791edca297c2f85fdb95d1f7549f9", + "size": 19634, + "normalized_sha256": "b3a546d58b1900847a01d08219a6668abdf791edca297c2f85fdb95d1f7549f9", + "format_sha256": "b3a546d58b1900847a01d08219a6668abdf791edca297c2f85fdb95d1f7549f9", + "format_kind": "gofmt" + }, + "internal/mcptools/overview_source_guard.go": { + "role": "index", + "sha256": "0cd9c00ef191763b8ba3ae0383385f7349949b68b0ce953d7152fbbe1948b5fa", + "size": 8166, + "normalized_sha256": "0cd9c00ef191763b8ba3ae0383385f7349949b68b0ce953d7152fbbe1948b5fa", + "format_sha256": "0cd9c00ef191763b8ba3ae0383385f7349949b68b0ce953d7152fbbe1948b5fa", + "format_kind": "gofmt" + }, "internal/mcptools/overview_split_evidence_test.go": { "role": "observe", "sha256": "81c60831b9daee549f55be0f5c6b6f880854ffad21c021a449f2dc776c23af34", @@ -4904,10 +4952,11 @@ "format_kind": "gofmt" }, "internal/mcptools/overview_volume_governance.go": { - "sha256": "28e382a8b3d49fa5b74816d317f0b72d7567f6ec1467de5259d1f760365867b6", - "size": 5166, - "normalized_sha256": "28e382a8b3d49fa5b74816d317f0b72d7567f6ec1467de5259d1f760365867b6", - "format_sha256": "28e382a8b3d49fa5b74816d317f0b72d7567f6ec1467de5259d1f760365867b6", + "role": "index", + "sha256": "ff5d10086a661802c0739152e0cf21119651eea84b7eef8a58daeded00424c89", + "size": 5527, + "normalized_sha256": "ff5d10086a661802c0739152e0cf21119651eea84b7eef8a58daeded00424c89", + "format_sha256": "ff5d10086a661802c0739152e0cf21119651eea84b7eef8a58daeded00424c89", "format_kind": "gofmt" }, "internal/mcptools/overview_volume_governance_test.go": { @@ -4935,10 +4984,11 @@ "format_kind": "gofmt" }, "internal/mcptools/server.go": { - "sha256": "c1e446143cab650219a04622851e3b5512030f3e08d0f838e0aa15c7cc823e6d", - "size": 12642, - "normalized_sha256": "c1e446143cab650219a04622851e3b5512030f3e08d0f838e0aa15c7cc823e6d", - "format_sha256": "c1e446143cab650219a04622851e3b5512030f3e08d0f838e0aa15c7cc823e6d", + "role": "index", + "sha256": "ce58c5104429c8c16eac81907a4c01d98ae446f5d29b25f5adbefdf40944be72", + "size": 12760, + "normalized_sha256": "ce58c5104429c8c16eac81907a4c01d98ae446f5d29b25f5adbefdf40944be72", + "format_sha256": "ce58c5104429c8c16eac81907a4c01d98ae446f5d29b25f5adbefdf40944be72", "format_kind": "gofmt" }, "internal/mcptools/service_binary.go": { @@ -5263,10 +5313,10 @@ }, "internal/mcptools/tools_read.go": { "role": "index", - "sha256": "0bd2d774f2bd8bd22d575a0c1b4f10fc5e7b803d5dc2d30c92bb7edb030ab873", - "size": 18987, - "normalized_sha256": "0bd2d774f2bd8bd22d575a0c1b4f10fc5e7b803d5dc2d30c92bb7edb030ab873", - "format_sha256": "0bd2d774f2bd8bd22d575a0c1b4f10fc5e7b803d5dc2d30c92bb7edb030ab873", + "sha256": "bb091f2fb6d7063c87a3ae0a4b398bfc1e777d46bf2ef4c3f267207d6e7bcc5d", + "size": 19557, + "normalized_sha256": "bb091f2fb6d7063c87a3ae0a4b398bfc1e777d46bf2ef4c3f267207d6e7bcc5d", + "format_sha256": "bb091f2fb6d7063c87a3ae0a4b398bfc1e777d46bf2ef4c3f267207d6e7bcc5d", "format_kind": "gofmt" }, "internal/mcptools/tools_read_lineending_test.go": { @@ -5287,10 +5337,10 @@ }, "internal/mcptools/tools_read_refresh.go": { "role": "index", - "sha256": "0ee9c5842f3ad7804da8cbc90dc839e96f780b1ed01d409d3cf0e2bcb7f660c3", - "size": 27964, - "normalized_sha256": "0ee9c5842f3ad7804da8cbc90dc839e96f780b1ed01d409d3cf0e2bcb7f660c3", - "format_sha256": "0ee9c5842f3ad7804da8cbc90dc839e96f780b1ed01d409d3cf0e2bcb7f660c3", + "sha256": "e00387deef0a4a3ee1cee963cfec6278a1516ef353f3df512344c55e023626a5", + "size": 28054, + "normalized_sha256": "e00387deef0a4a3ee1cee963cfec6278a1516ef353f3df512344c55e023626a5", + "format_sha256": "e00387deef0a4a3ee1cee963cfec6278a1516ef353f3df512344c55e023626a5", "format_kind": "gofmt" }, "internal/mcptools/tools_read_refresh_test.go": { @@ -5319,10 +5369,10 @@ }, "internal/mcptools/tools_read_volumes.go": { "role": "index", - "sha256": "7fe165dea6f0ce0908c239c3485ee3fa012f6f37a5350030f86bd81b4db8b97f", - "size": 8414, - "normalized_sha256": "7fe165dea6f0ce0908c239c3485ee3fa012f6f37a5350030f86bd81b4db8b97f", - "format_sha256": "7fe165dea6f0ce0908c239c3485ee3fa012f6f37a5350030f86bd81b4db8b97f", + "sha256": "55d8a52bc64497e9d0e7bb66d7751ee1e94e1280496dfae82c259801c5c27cca", + "size": 11849, + "normalized_sha256": "55d8a52bc64497e9d0e7bb66d7751ee1e94e1280496dfae82c259801c5c27cca", + "format_sha256": "55d8a52bc64497e9d0e7bb66d7751ee1e94e1280496dfae82c259801c5c27cca", "format_kind": "gofmt" }, "internal/mcptools/tools_read_volumes_test.go": { @@ -6443,10 +6493,10 @@ }, "internal/volumegovernance/facts.go": { "role": "index", - "sha256": "4ed90c349d01d227fddb49c3d4c08d0355e5a0854cc98f9cba5d105195479b8a", - "size": 45275, - "normalized_sha256": "4ed90c349d01d227fddb49c3d4c08d0355e5a0854cc98f9cba5d105195479b8a", - "format_sha256": "4ed90c349d01d227fddb49c3d4c08d0355e5a0854cc98f9cba5d105195479b8a", + "sha256": "7e45c84ba18f1e1e920282297564b0b8cb0466b76047d7978e756100c4856c16", + "size": 45982, + "normalized_sha256": "7e45c84ba18f1e1e920282297564b0b8cb0466b76047d7978e756100c4856c16", + "format_sha256": "7e45c84ba18f1e1e920282297564b0b8cb0466b76047d7978e756100c4856c16", "format_kind": "gofmt" }, "internal/volumegovernance/facts_test.go": { @@ -6625,9 +6675,9 @@ }, "scripts/blackbox/mcp_scenarios.py": { "role": "index", - "sha256": "7b53ab68c6727da52987e4dbbbce9104dc25a37a08c3fd62da147bbd1187e23c", - "size": 128549, - "normalized_sha256": "7b53ab68c6727da52987e4dbbbce9104dc25a37a08c3fd62da147bbd1187e23c" + "sha256": "eb51198d6178213dd8251063ead1895d53751ce4459723666d2ac611e3ca9d0e", + "size": 130568, + "normalized_sha256": "eb51198d6178213dd8251063ead1895d53751ce4459723666d2ac611e3ca9d0e" }, "scripts/blackbox/mcp_upgrade.py": { "role": "index", @@ -6860,9 +6910,9 @@ }, "spec/public/aoci-overview-delivery-v1.txt": { "role": "index", - "sha256": "e593b80c5225269b9ce090709fc0b8364fe4c325f25e627180301821500735f8", - "size": 17888, - "normalized_sha256": "e593b80c5225269b9ce090709fc0b8364fe4c325f25e627180301821500735f8" + "sha256": "10c77430c8d4d840415863852c8bcb1662bb3b4771b5f67a70a95e2ba6b497dc", + "size": 21224, + "normalized_sha256": "10c77430c8d4d840415863852c8bcb1662bb3b4771b5f67a70a95e2ba6b497dc" }, "spec/public/aoci-safe-inventory-and-scope-refresh-v1.txt": { "role": "index", @@ -7608,9 +7658,9 @@ }, "textassets/en-US/contracts/mcp/tools/overview-description.txt": { "role": "observe", - "sha256": "1cdc1c4699a7352e1365b87906413039982798b25c4d77ea1c249b0c97daa896", - "size": 3168, - "normalized_sha256": "1cdc1c4699a7352e1365b87906413039982798b25c4d77ea1c249b0c97daa896" + "sha256": "83c6e4757512fe5b3eed090b4805443c74277a0e8eca8243efe7dbc1d845de93", + "size": 3407, + "normalized_sha256": "83c6e4757512fe5b3eed090b4805443c74277a0e8eca8243efe7dbc1d845de93" }, "textassets/en-US/contracts/mcp/tools/remove-entry-description.txt": { "role": "observe", @@ -7650,9 +7700,9 @@ }, "textassets/en-US/contracts/ui/messages.json": { "role": "observe", - "sha256": "90bd6044dc0e46eda3995b63a4452e07b3ecaf0652ce56c02b8f4c0f2902c222", - "size": 195408, - "normalized_sha256": "90bd6044dc0e46eda3995b63a4452e07b3ecaf0652ce56c02b8f4c0f2902c222" + "sha256": "3aeb6d93365630c9da9d716890a6a7a336fc337bc373617383fb33c0d07d26fa", + "size": 195679, + "normalized_sha256": "3aeb6d93365630c9da9d716890a6a7a336fc337bc373617383fb33c0d07d26fa" }, "textassets/en-US/prompts/ai-probe-system.txt": { "role": "observe", @@ -8716,9 +8766,9 @@ }, "textassets/zh-CN/contracts/mcp/tools/overview-description.txt": { "role": "observe", - "sha256": "bd83240bf4dcf2e774e2cf0c8477e26abe3117e0cf8f09d8e8ea64002d47a39f", - "size": 2721, - "normalized_sha256": "bd83240bf4dcf2e774e2cf0c8477e26abe3117e0cf8f09d8e8ea64002d47a39f" + "sha256": "52fcfdd90bc8ba43d4814b316f8d464bd28a3a9b6c0ccea93af2f2a8fc11d481", + "size": 2957, + "normalized_sha256": "52fcfdd90bc8ba43d4814b316f8d464bd28a3a9b6c0ccea93af2f2a8fc11d481" }, "textassets/zh-CN/contracts/mcp/tools/remove-entry-description.txt": { "role": "observe", @@ -8758,9 +8808,9 @@ }, "textassets/zh-CN/contracts/ui/messages.json": { "role": "observe", - "sha256": "02c2049167b88b0f2fe6785c2cf6f0ec2140fda296bbcfc31824b9ca72831848", - "size": 178134, - "normalized_sha256": "02c2049167b88b0f2fe6785c2cf6f0ec2140fda296bbcfc31824b9ca72831848" + "sha256": "55a88ea0ab695a4d67645f61e23f864ff08ddc43611eaca948720bbcda0d6069", + "size": 178415, + "normalized_sha256": "55a88ea0ab695a4d67645f61e23f864ff08ddc43611eaca948720bbcda0d6069" }, "textassets/zh-CN/prompts/ai-probe-system.txt": { "role": "observe", diff --git a/aoci.code.txt b/aoci.code.txt index 15d1df71..88670957 100644 --- a/aoci.code.txt +++ b/aoci.code.txt @@ -272,6 +272,7 @@ lexical.go[CG7T]: F:Defines shared machine-owned lexical tokens and the remediat managed_scope.go[CG8S]: F:Defines managed-scope roles, structured decision bases, profiles, policy, evaluation, approval, and transaction tokens | R:- | A:- | S:- numeric.go[CG9M]: F:Owns cross-layer numeric limits for FRAS, batching, budgets, refresh, and delivery | R:- | A:- | S:Cross-layer numeric limits live only here: the Code batch cap default (50) sits below the wire ceiling (200) it is validated against, the Maintain transport budget defaults to 24 KiB within 16 to 192 KiB with a 40 KiB Claude Code init value, the transport list limit is 20, and the Database Evidence byte gate defaults to 64 KiB; HighImportanceMinC (7) and the 25 percent S-absent hint threshold are read by the coverage report, the hint, and the optimization selector alike status.go[CG9M]: F:Defines stable machine status, error, recovery, and next-action vocabularies | R:code:internal/cli/index_agent_guide.go,code:internal/dbcognition/state.go,code:internal/databasebootstrap/prepare.go | A:ActionCallNoArgumentMaintainForCurrentMachineBatch,DatabaseCognitionActionMaintain | S:Database Cognition maintenance aliases the exact no-argument Maintain action used by Guide; no database-specific write path is introduced +overview_session.go[CG8T]: F:Defines ephemeral Overview cursor version and server retention limits | R:code:internal/mcptools/overview_session_v2.go | A:- | S:These runtime bounds do not alter project policy, cognition identity, chunk receipts or scoring ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/internal/managedscope/=== evaluate.go[CG8M]: F:Evaluates Safe Inventory objects against profile, project, Curation, Git, and safety rules | R:code:internal/managedscope/policy.go | A:- | S:Exclude paths are never content-read; path matching uses exact Git semantics on every host, so the applied identity never depends on the filesystem that computed it; the identity preimage keeps its case-sensitive form, and high-risk opt-ins remain approval-gated before Snapshot @@ -282,21 +283,21 @@ types.go[CG5T]: F:Defines managed-scope rules with decision bases, evaluations, starter.go[CG7T]: F:Defines the seven starter observe rules that init persists for vendored static assets and generated bundles under the production profile | R:code:internal/managedscope/policy.go,code:internal/config/scope_budget.go | A:StarterRules,StarterRuleIDPrefix,StarterRuleCreatedBy | S:Source builtin: profile rules outrank, any user rule overrides; persisted at init, not built in, since built-in rules sit outside the policy identity and would flip roles in old repositories ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/internal/managedstate/=== -state.go[CG9S]: F:Loads the active Legacy or Managed Scope snapshot and detects Code drift only when policy and budget receipts align | R:code:internal/baseline/scope_detect.go,code:internal/managedscope/evaluate.go | A:Load,Detect,CurationExclusions | S:Direct policy edits fail closed as scope_change_required; a receipt recorded under the historical case-insensitive semantics is never adopted and migrates through Scope Change; empty budget identity is accepted only for early receipt compatibility. The loaded Baseline is passed to the snapshot for digest reuse, which changes no value +state.go[CG9S]: F:Loads the active Legacy or Managed Scope snapshot, reuses aligned Baseline digests, and detects Code drift only when policy and budget receipts align | R:code:internal/baseline/scope_detect.go,code:internal/managedscope/evaluate.go | A:Load,Detect,CurationExclusions | S:Direct policy edits fail closed as scope_change_required; a receipt recorded under historical case-insensitive semantics is never adopted and migrates through Scope Change; empty budget identity is accepted only for early receipt compatibility. The loaded Baseline is passed to the snapshot for digest reuse, which changes no value ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/internal/mcptools/=== cognition.go[CG8M]: F:Defines Legacy and Volumes cognition receipts, refresh projection, and host-model reliability assessment | R:code:internal/mcptools/tools_read.go,code:internal/mcptools/tools_read_volumes.go,code:internal/mcptools/overview_delivery_v1.go | A:- | S:Receipt identity can invalidate cognition but cannot declare model reliability; only host-model valid state with matching repository, service, scope, and cognition identity can suppress full recall, and write paths cache their proven aligned identity for the read-tool line overview_attestation_v1.go[CG9L]: F:Builds and verifies identity-bound Whole-Index challenge attestations and receipts | R:- | A:- | S:The challenge samples deterministic strata of the exact formal Entry sequence; pass needs the current Index, sequence, count, digest, ordered answers with at least 80% fully correct and at most one identity miss, core F judged by normalized token similarity with CJK bigrams, and complete delivery claims; fail is only a foreign envelope or zero correct, otherwise partial; a repeated ordinal is scored once, so passed can never exceed total -overview_delivery_v1.go[CG9L]: F:Chunks complete formal cognition deterministically and validates continuation cursors and host receipts | R:- | A:- | S:The first full request must omit host confirmation and model attestation; either field selects attestation-only without retransmitting the body, while cursors bind snapshot, chunk budget, ordinal, and previous chunk; the session remembers each proof half per body so both may arrive in separate calls in either order, an explicit half supersedes memory, and a fresh complete delivery resets it -server.go[CG8M]: F:Registers the public AOCI MCP server and nine tools while carrying Code batch plans through structured failure handling | R:code:internal/cognitiontxn/transaction.go | A:- | S:Tool names and count are fixed at nine; every call reloads repository state with no process cache, stdout stays JSON-RPC-only, and panics become structured MCP errors; startup records the service binary's disk identity; repoCtx.dict carries the dictionary text (empty means Legacy); the loader stops only on layout transactions, since MCP write receipts are closed by the tools that wrote them +overview_delivery_v1.go[CG9L]: F:Frames canonical cognition and renders deterministic chunks and proof responses from optional immutable prepared state | R:code:internal/mcptools/overview_session_v2.go,code:internal/mcptools/overview_attestation_v1.go | A:- | S:Prepared frame, Challenge and spans preserve body bytes and ordinals. Proof-only output merges halves bound to one body; fresh delivery resets evidence. Schema rejection never reaches semantic scoring +server.go[CG8M]: F:Builds the nine-tool MCP server with repository loading, schema diagnostics and structured failures | R:code:internal/cognitiontxn/transaction.go,code:internal/mcptools/overview_diagnostics.go | A:- | S:Each call reloads formal state; only live-guarded immutable V2 delivery is reused. Stdout is JSON-RPC-only. Panic returns structured failure. Startup binds binary disk identity; layout transactions block loading while owning write tools retain recovery closure tools_maintain.go[CG9L]: F:Implements the primary Maintain tool and routes Legacy or Volumes alignment workflows | R:- | A:- | S:- tools_maintain_volumes.go[CG9L]: F:Builds bounded Code and Database authoring batches, review closures, candidate bindings, and model-authoring contracts from shared governance facts | R:code:internal/codebatch/receipt.go,code:internal/dbcognition/receipt.go,code:internal/volumegovernance/facts.go,code:internal/mcptools/tools_next_commands.go,code:internal/mcptools/tools_maintain_budget.go | A:- | S:One current machine batch cut by the transport budget under the team cap; remaining needs a new Maintain preimage; candidates are model-only and source/Evidence bound; Root/Meta are guards; with candidates, governance lists and the review and write sets are five-item samples with counts and code_plan carries no candidates; blocked or evidence_required results carry next_commands; candidates and total_targets come from one CodeAuthoringWorkFor value, so held sources are never candidates; a response naming candidates carries authoring_meta; an unspellable directory withholds the batch -tools_read.go[CG8L]: F:Registers and serves MCP Rules, Overview, Entry retrieval, and Search across Legacy and Volume cognition layouts | R:code:internal/mcptools/overview_attestation_v1.go,code:internal/mcptools/overview_volume_governance.go,code:docs/overview-delivery.md | A:aoci_rules,aoci_overview,aoci_get_entries,aoci_search | S:Ordinary Overview delivers the complete requested dependency-closed cognition bytes, chunked only as transport; pending Recovery or snapshot drift fails closed; the aoci_overview _meta result-size declaration is static, raising only that tool's Host persist threshold, never a capability probe; path refusals render their catalog reason +tools_read.go[CG8L]: F:Registers Rules, Overview, Entry retrieval and Search across Legacy and Volume layouts | R:code:internal/mcptools/tools_read_volumes.go,code:internal/mcptools/overview_attestation_v1.go,code:docs/overview-delivery.md | A:aoci_rules,aoci_overview,aoci_get_entries,aoci_search | S:Ordinary Overview delivers dependency-closed cognition even when dirty, with explicit reliability. Recovery and drift fail closed. Host identity isolates V2 delivery. Schema acceptance does not imply scoring; path refusals render catalog reasons and result-size declaration is a static threshold rather than a capability probe tools_write.go[CG8L]: F:Plans, validates, CAS-writes, and baselines a single Legacy-index Entry update under the shared Index lock | R:code:internal/mcptools/tools_write_api.go,code:internal/baseline/baseline.go,code:internal/cognitiontxn/transaction.go | A:- | S:The Index postimage precedes Baseline save; if Baseline persistence fails, retry the same candidate. Budget rejections carry candidate-bound findings; a dictionary refusal quotes the rejected spelling in a form the active Locale accepts or is discarded whole; threshold checks read the context's dictionary text (Meta or index header); refuses on any pending receipt; a refused path names its catalog reason tools_write_batch.go[CG9L]: F:Validates and applies one complete semantic candidate batch across affected Volumes | R:- | A:- | S:All candidates plan against one preimage and fail as a unit before writes; apply uses one locked CAS transaction, source/guard rechecks, postimage confirmation, Baseline advancement, and retained recovery so partial post-write failure remains recoverable rather than rolled back; a refused item path names its catalog reason overview_cognition_level.go[CG7T]: F:Derives additive cognition delivery and verification levels without conflating governance | R:- | A:- | S:- overview_cognition_state_v2.go[CG8S]: F:Projects independent delivery, usability, attestation, governance, and reliability cognition state | R:- | A:- | S:- -overview_volume_governance.go[CG7S]: F:Calculates scope-specific Volume alignment and reliability for Overview responses | R:code:internal/volumegovernance/facts.go | A:- | S:Refresh identity excludes BusinessSource and Exclude counts but binds Baseline bytes; a new Baseline cannot reuse an older delivered body; blocker count omits informational findings +overview_volume_governance.go[CG7S]: F:Adapts shared governance and its actual assessed Code snapshot into Overview refresh and reliability facts | R:code:internal/volumegovernance/facts.go | A:- | S:Identity binds Baseline bytes but excludes commit-only BusinessSource and runtime exclude counts. Informational findings do not block; snapshot exposure preserves the full verdict tools_auto_alignment.go[CG7M]: F:Re-evaluates post-Apply Legacy or Volumes governance and returns alignment, remaining findings, and the current cognition receipt | R:code:internal/managedstate/state.go,code:internal/curation/classify.go,code:internal/mcptools/tools_write_api.go,code:internal/volumegovernance/facts.go | A:- | S:Volumes Code drift uses the Managed Scope snapshot with formal assets removed and the same ClassifyCodeDrift Verify uses, so Observe, Exclude, and held sources never become Entry debt tools_auto_audit.go[CG7T]: F:Builds a compact deterministic P-23 audit summary from an update batch and optional AtomicBatch outcome | R:code:internal/mcptools/tools_write_api.go,code:internal/mcptools/tools_write_batch.go | A:- | S:The summary hashes the exact submitted items, but its presence is audit evidence rather than governance or formal-write proof tools_auto_result.go[CG8M]: F:Defines and serializes compact applied, repair, stopped, audit, governance, plan, optimization, and terminal-action MCP results | R:code:internal/mcptools/tools_write_api.go,code:internal/mcptools/tools_cognition_optimization.go,code:internal/codebatch/types.go,code:textassets/maintain_assets.go | A:autoResult,renderAutoResult,autoApplyNextAction | S:Idempotent duplicates report Applied=0; the final ordinary Volumes Apply selects a dedicated proof action and carries next_commands with its runnable spellings, while paged, Legacy, and Optimization flows retain their own continuations @@ -305,10 +306,10 @@ tools_format_only.go[CG7S]: F:Advances Baseline for proven format-only or line-e tools_header.go[CG7T]: F:Implements the MCP Header dictionary reader for Legacy and Volumes layouts | R:- | A:- | S:Volumes mode returns exact Meta bytes with Root/Meta identities plus the session cognition line; pending delivery fails closed and each read appends header_show to the Ledger tools_maintain_curation_state.go[CG7T]: F:Projects Curation Missing classification into pending authoring targets and non-pending technical skips for Maintain | R:code:internal/curation/classify.go,code:internal/mcptools/tools_maintain.go | A:- | S:A skipped path that is also pending remains pending and is never duplicated as a technical skip tools_maintain_database.go[CG8M]: F:Parses Maintain inputs and dispatches Legacy, ordinary Volumes, explicit Database, and aligned-entry optimization requests to their evidence-bound workflows | R:code:internal/mcptools/tools_maintain.go,code:internal/mcptools/tools_maintain_volumes.go,code:internal/mcptools/tools_cognition_optimization.go,code:internal/dbcognition/receipt.go | A:- | S:Ordinary Volumes requests with omitted scope or explicit code/all must use shared Volume governance and never fall through Legacy loading; cognition_optimization accepts only Volumes v1 Code and object_refs require that intent, while explicit database remains offline over saved Evidence -tools_read_refresh.go[CG7L]: F:Computes cognition checkpoint reasons, generations, refresh guidance, the session cognition line for reads, and per-body delivery evidence memory | R:- | A:- | S:Session facts only, zero scans; invalid is reserved for identity breaks, so a reliable receipt under a pending refresh reads uncertain; the proof halves latch across calls in any order +tools_read_refresh.go[CG7L]: F:Tracks refresh reasons, generations, reliability, proof halves and the server-local delivery store | R:code:internal/mcptools/overview_session_v2.go | A:- | S:Bookkeeping performs no source scan. Identity breaks invalidate receipts; pending refresh makes them uncertain. Confirmed aligned attempts consume reasons even with partial cognition tools_read_search.go[CG7S]: F:Searches Legacy or Volume cognition while preserving canonical object identities | R:- | A:- | S:- tools_read_snapshot.go[CG7T]: F:Enumerates active formal cognition assets and revalidates their exact bytes after rendering to prevent mixed delivery | R:code:internal/mcptools/tools_read.go,code:internal/mcptools/transaction_guard.go | A:- | S:A changed byte, unsafe file type, or unreadable participant invalidates the entire delivery snapshot; this layer does not merge or repair images -tools_read_volumes.go[CG7M]: F:Delivers scoped Volumes Overview bodies, receipts, chunking, attestation targets, refresh accounting, and governance snapshot checks | R:code:internal/mcptools/tools_read.go,code:internal/mcptools/overview_attestation_v1.go,code:internal/mcptools/overview_volume_governance.go | A:aoci_overview | S:Pending recovery or a changed cognition or governance snapshot fails closed; every explicit Overview re-delivers the complete available scope, and chunk ordinals share the formal Entry sequence +tools_read_volumes.go[CG8M]: F:Routes scoped Overview through immutable guarded V2 delivery or compatible live-governance rendering | R:code:internal/mcptools/overview_session_v2.go,code:internal/mcptools/overview_volume_governance.go | A:aoci_overview | S:Eligible initial requests assess and build once; continuation rebuilds neither. Host binding uses SDK object identity. Unknown V2 state never reconstructs. V1 and time-dependent Database paths retain live checks. Drifted proofs require current governance. Oversize V2 tokens reject before splitting tools_remove.go[CG7L]: F:Implements MCP orphan Entry removal and routes layout-specific guarded pipelines | R:code:internal/mcptools/tools_remove_volumes.go,code:internal/cognitiontxn/transaction.go,code:internal/index/editor_remove.go | A:- | S:MCP removal is orphan-only, the CLI may remove a live Entry; a plain path under Volumes v1 routes as code:; both recheck, use CAS, fail closed on drift; path refusals carry catalog reasons tools_remove_volumes.go[CG7L]: F:Plans, proves, commits, and recovers explicit Code or Database Volume Entry removal with Baseline and evidence guards | R:code:internal/volumegovernance/facts.go,code:internal/cognitiontxn/transaction.go,code:internal/index/editor_remove.go | A:- | S:Orphans only; Code lines go by path so twins stay; a pending scope change names scope activate; a disabled Ledger refuses pre-write; old receipt: absent→superseded, unchanged→discard, changed→refuse tools_report.go[CG7S]: F:Records bounded follow-up reports when evidence cannot support reliable semantic authoring | R:- | A:- | S:Under Volumes v1 the tool answers volume_read_only; a refused path names its catalog reason @@ -325,6 +326,9 @@ overview_probe_v1.go[CG7S]: F:Issues and grades the deterministic two-question c tools_next_commands.go[CG8S]: F:Composes runnable next_commands for terminal, blocked, and evidence-required MCP results from the shared remediation suffixes and the server's own executable | R:code:internal/machinecontract/lexical.go,code:internal/mcptools/tools_maintain_volumes.go,code:internal/mcptools/tools_write_api.go | A:- | S:The prefix is the running server's absolute quoted path because aoci is whatever the host MCP config names and need not be on PATH; an unresolvable executable omits the field, since an absent advisory beats a wrong command; recovery_pending points at the Guide, plus scope status for a scope receipt; scope_change_required names status then activate overview_plan_export.go[CG5T]: F:Exports the exact Overview chunk planner and the complete Overview body so a status surface reports what delivery would do | R:code:internal/mcptools/overview_delivery_v1.go,code:internal/ui/snapshot.go | A:PlanOverviewChunks,OverviewChunkPlan,OverviewBody | S:Both run the delivery path's own builder and touch no session, Ledger, or receipt; a second concatenation of the Volumes would drift from what an agent receives tools_maintain_budget.go[CG8S]: F:Sizes one Maintain response: reserves fixed and sampled parts, measures each candidate on the wire, cuts the batch to the transport budget, projects the plan | R:code:internal/codebatch/receipt.go,code:internal/mcptools/tools_maintain_volumes.go,code:internal/cli/scan_skipped.go,code:internal/cli/index_agent_guide.go | A:PlanCodeBatchIncluded,EstimateCodeAuthoringRounds | S:The base reservation covers only path-free parts; Meta and instruction bytes are measured on the repository and the five-item review, write, findings, and missing samples on the candidates, so a custom Meta or a deep tree never overflows a constant; optimization candidates count 320 extra bytes +overview_diagnostics.go[CG8T]: F:Augments SDK Attestation schema errors and separates completion and confirmation Ledger events | R:code:internal/mcptools/server.go,code:internal/mcptools/overview_delivery_v1.go | A:- | S:SDK validates before the typed handler; only its argument-validation errors receive the diagnostic. object_identity is required; object and object_ref remain invalid. Schema rejection makes no semantic verification claim +overview_session_v2.go[CG9M]: F:Retains immutable Host-owned deliveries and serves authenticated continuations through live input guards | R:code:internal/mcptools/overview_source_guard.go,code:internal/machinecontract/overview_session.go,code:internal/mcptools/overview_delivery_v1.go | A:- | S:Weak SDK object identity prevents Host address reuse without retaining closed transports. Random id and per-delivery HMAC authenticate immutable cursor positions. Count, payload plus proof bytes, idle and absolute TTL bound retained state. Proof growth reserves a conservative high-water cost before merge and may evict peers; proof lookup counts as activity. Evicted in-flight snapshots may finish, but future lookups reject +overview_source_guard.go[CG9M]: F:Binds current governance inputs and rejects prepared deliveries after formal assets, source bytes or inventory topology change | R:code:internal/managedstate/state.go,code:internal/fs/safe_inventory.go | A:- | S:Hashes formal, config, curation and observe/index bytes; topology covers unsafe boundaries, Git membership and repository identity. Canonical boundary encoding accepts unchanged tracked symlinks and gitlinks but rejects topology drift. Excluded and submodule content remain unread; pending Recovery or AtomicWrite fails closed; Database and unsupported snapshots retain live checks ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/internal/scopechange/=== authorization.go[CG8L]: F:Builds and verifies managed-scope authorization and partitions every auto blocker into reasons independent review may ratify and safety boundaries it may not | R:code:internal/scopechange/plan.go,code:internal/scopechange/transaction.go | A:EffectiveApplyAuthorizationMode | S:Scope Change and first Baseline establishment must resolve the effective mode through one function so the receipted and authorizing postures cannot diverge; Resume and idempotent re-apply select the same branch as Apply; an unclassified blocker fails the build. One unratifiable blocker denies the reviewer route entirely, because a transport constraint only ever accompanies a ratifiable one @@ -333,7 +337,7 @@ transaction.go[CG8L]: F:Applies scope policy, Entry, Baseline, Observe, Curation types.go[CG5M]: F:Defines scope candidates, coverage-reduction, line-ending, stale-retention, and posture evidence, risk, Plan, Preview, authorization, and result types | R:code:internal/scopechange/plan.go | A:- | S:SourceLineEndingOnly and SourceStaleRetained are omitempty because planIdentity hashes the whole Plan, so serializing either empty would change every existing plan_id and strand in-flight approvals ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/internal/volumegovernance/=== -facts.go[CG9L]: F:Computes one deterministic governance fact set shared by Verify, Check, Guide, Maintain, and Overview | R:code:internal/baseline/detect.go,code:internal/businesssource/manifest.go,code:internal/curation/classify.go,code:internal/cognitiontxn/transaction.go | A:ClassifyCodeDrift | S:Verify, Check, and Guide consume the complete fact set; BoundListsForTransport projects the Maintain copy with samples plus counts, bounds budget violations, and drops current database items; finalize treats an unclassified finding as blocked and Informational names the codes it ignores; ClassifyCodeDrift classifies raw Missing once for Assess and the post-Apply inspector: held sources become code_skipped or code_curation_excluded, so CodeAuthoringWorkFor plans what Verify counts; every pending receipt is its own recovery_pending finding; BudgetFacts tallies S coverage over Code objects only +facts.go[CG9L]: F:Computes shared deterministic governance and exposes the exact assessed Code snapshot for guarded reuse | R:code:internal/baseline/detect.go,code:internal/businesssource/manifest.go,code:internal/curation/classify.go,code:internal/cognitiontxn/transaction.go | A:Assess,AssessWithSnapshot,ClassifyCodeDrift | S:AssessWithSnapshot removes no check and changes no verdict. Full consumers keep complete facts; transport bounds lists. Unknown findings block, explicit Informational codes do not. Missing classification distinguishes held sources from debt; pending receipts remain Recovery findings escale.go[CG7T]: F:Audits every Code Entry's E scale letter against its source line count for verify and check, reporting mismatches as facts that never fail | R:code:internal/index/escale.go,code:internal/cli/check.go,code:internal/cli/verify.go | A:AssessEScale,EScaleFacts,EScaleItem | S:Reads every indexed file, so verify and check call it and Assess never does; without Meta thresholds or on an unreadable source nothing is judged; items carry the letter, expected letters, and lines ===/home/alkor2000/aoci-code-public-staging-phase1/public-candidate/scripts/blackbox/longrun-refresh/=== @@ -377,7 +381,7 @@ aoci-index-format-v1.txt[CG8S]: F:Defines the public text interoperability contr aoci-managed-scope-and-budget-v1.txt[CG8M]: F:Specifies governed roles, authoring debt, coverage-reduction protection, ratifiable blockers, host-independent paths, posture, transactions, and budgets | R:code:internal/scopechange/authorization.go,code:internal/managedscope/evaluate.go | A:- | S:Refusing auto is only half the contract: every blocker independent review may ratify also sets interaction_required, while safety boundaries carry no reviewer and must be resolved instead; under Volumes the candidate set carries policy only and a changed Index source is retained under source_stale_retained, not blocked; an ungoverned git-ignored path is never part of the Envelope aoci-mcp-runtime-v1.txt[CG8T]: F:Specifies MCP server identity, stdio JSON-RPC behavior, and the stable nine-tool surface | R:- | A:- | S:- aoci-object-fras-v2.txt[CG8T]: F:Specifies canonical object identities, expanded starter dictionaries, compact tags, and FRAS v2 field limits | R:code:spec/public/aoci-cognition-volumes-v1.txt | A:repository-cognition-object/v2,compact A+B+C+[D]+E | S:Starter G means cross-domain and Z requires understood evidence with no named fit; insufficient evidence is never Z or an S constraint, and existing formal Meta remains authoritative -aoci-overview-delivery-v1.txt[CG8M]: F:Specifies Overview chunking, receipts, challenge ordinals, the pass share with semantic core F matching, split proof submission, and delivery failure handling | R:- | A:- | S:Concrete localized outcome sentences live in the locale text assets, not this contract +aoci-overview-delivery-v1.txt[CG8M]: F:Defines canonical Overview delivery, guarded V2 continuation and proof retention bounds, split proofs and strict diagnostics | R:code:internal/mcptools/overview_session_v2.go,code:internal/mcptools/overview_diagnostics.go | A:- | S:Canonical bytes and receipts remain v1; only opaque cursor changes version. V2 expiry, eviction, foreign Host or restart requires fresh delivery. V1 reconstructs. Retained proof growth shares the byte cap and oversize fails before scoring. Database remains live. Host schema flattening is outside the server, so description fallback does not prove its repair aoci-safe-inventory-and-scope-refresh-v1.txt[CG8S]: F:Specifies safe filesystem inventory and atomic managed-scope refresh behavior | R:- | A:- | S:Collapsing an ignored directory into one exclusion is allowed only when every path beneath it is already a hard exclusion, so no candidate, opt-in, rule outcome, or identity changes; artifact names are configured, never built in: exclude_root_dirs for new repositories (root and git-ignored at any depth), exclude_dirs for old ones; an absent root list keeps the old identity; a configured exclusion names its entry aoci-system-cognition-runtime-v1.txt[CG8T]: F:Specifies narrow relation, lineage, impact, and evolution projections over governed cognition | R:- | A:- | S:All projections are deterministic, sorted, derived, and read-only: they create no Apply, CAS, Recovery, Baseline, Ledger, approval, or persistence lifecycle and never invent missing relations s-field-discipline.txt[CG8S]: F:Defines the admission rule and quota discipline for non-inferable safety facts in S fields | R:code:internal/index/quota.go | A:- | S:The two machine gates differ on purpose: the warning-level curation check honours a declaration both ways, while the error-level object gate honours only a looser one; the C6+ look at where behaviour is decided is writing discipline the machine never judges @@ -623,7 +627,7 @@ README.md[CG5M]: F:Documents the four Python suites and the Go longrun runner: l generate_repo_c.py[CG5L]: F:Generates the frozen 453-file layered fixture that lets the scale suite reach the real batch limit with realistic cross-batch relations | R:- | A:python3 scripts/blackbox/generate_repo_c.py | S:Regenerating changes the fixture identity the scale suite asserts; generated names must avoid the built-in sensitive and runtime patterns, and a NUL byte makes Curation silently skip the file mcp_conformance.py[CG8L]: F:Drives the built binary as a real MCP stdio client and checks the wire surface: handshake, nine tools, schemas, response shapes, malformed input, locale twins | R:code:scripts/blackbox/README.md,code:README.md,code:README.zh-CN.md,code:internal/mcptools/server.go,code:scripts/blackbox/stdio_deadline.py,code:scripts/blackbox/stdio_capture.py | A:python3 scripts/blackbox/mcp_conformance.py,AOCI_REPO,AOCI_BIN,AOCI_EXPECT_VERSION | S:Read-only on the host repository, which must be aligned with a multi-chunk Overview; the 48 KiB host window is measured on a real host; the published count (53) is a suite property and the run fails when a document disagrees; the Overview reader follows the exact cursor chain; the zh-CN lane builds en-US and zh-CN twin fixtures in a temp dir and demands identical machine fields with localized sentences mcp_lifecycle.py[CG8L]: F:Runs init-to-realignment lifecycles over three frozen fixtures, bring-up lanes for submodules, root lists, removal, machine documents, zh-CN, and a model track | R:code:scripts/blackbox/README.md,code:scripts/blackbox/generate_repo_c.py,code:scripts/blackbox/mcp_scenarios.py,code:internal/cli/init.go | A:python3 scripts/blackbox/mcp_lifecycle.py,--suites,--model,--compare,--mysql-image | S:repo-a/b cover drift and re-alignment, repo-c multi-batch authoring at the raised limit; the database suite needs Docker and takes --mysql-image, mapping the fixture's two 8.0-only statements to 5.7 in memory; committed fixtures must be named; governance walks the pending-policy path and held-source probes to aligned; the submodule lane needs protocol.file.allow; the import scratch dir is removed at exit -mcp_scenarios.py[CG8L]: F:Drives hostile handling of fixtures: cursors, write rejection, crashes, races, Scope Change, activation, held sources, batches, next commands, page, hook | R:code:scripts/blackbox/README.md,code:README.md,code:README.zh-CN.md,code:internal/mcptools/tools_next_commands.go,code:internal/mcptools/tools_maintain_budget.go,code:internal/ui/server.go,code:internal/cli/hook.go | A:python3 scripts/blackbox/mcp_scenarios.py,--group,--list,AOCI_SCENARIO_WORK,AOCI_SCENARIO_KEEP | S:A writing scenario builds its own fixture; the host repository stays read-only; budget, Guide batch, and next-command group are measured; past 50 per batch the budget is raised too; tty needs a real pty; paths are slash-normalized on Windows; Scope Change over a changed source: Volumes retains, Legacy blocks; O2 pins S-absent, E mismatch, C; a --group run skips the published-count check +mcp_scenarios.py[CG8L]: F:Exercises hostile MCP cursor, write, recovery, concurrency, scope, activation, paging and budget scenarios | R:code:scripts/blackbox/README.md,code:internal/mcptools/tools_next_commands.go,code:internal/ui/server.go | A:python3 scripts/blackbox/mcp_scenarios.py,--group,--list,AOCI_SCENARIO_WORK,AOCI_SCENARIO_KEEP | S:Disposable writing fixtures keep Host read-only. V2 tamper, restart and Baseline drift reject; V1 reconstructs compatible chunks. O2 preserves E-scale ranking between absent-S and C ordering. Host-window and batch budgets stay measured; TTY needs a pty. Scope Change preserves Volumes and blocks Legacy when sources changed mcp_upgrade.py[CG8L]: F:Proves the upgrade axis both ways: repositories authored by released binaries stay governable by the binary under test, and each release reads one it wrote | R:code:scripts/blackbox/README.md,code:internal/cognitionbudget/policy.go,code:internal/config/scope_budget.go,code:.github/workflows/full-confidence.yml,code:scripts/blackbox/stdio_deadline.py,code:scripts/blackbox/stdio_capture.py,code:internal/index/parser.go | A:python3 scripts/blackbox/mcp_upgrade.py,--versions,--recent,--self,--allow-offline,AOCI_UPGRADE_CACHE | S:Only nobudget reaches LegacyPolicy; spacedroot and cutsegment carry roots truncated up to rc13; inflight applies an unsubmitted batch via MCP; legacydirs pins the released policy's roles: any-depth to rc19, root list from rc20; downgrade expects aligned from rc20, before it a named scope_change_required refusal with zero formal writes; --self labels the candidate from the CHANGELOG top; fetch failures fail the run stdio_deadline.py[CG5T]: F:Wraps MCP RPC I/O in a wall-clock watchdog that kills and reaps the server on expiry and includes captured stderr in its method-specific TimeoutError | R:code:scripts/blackbox/mcp_conformance.py,code:scripts/blackbox/mcp_scenarios.py,code:scripts/blackbox/mcp_upgrade.py | A:rpc_deadline | S:A clock check around readline cannot interrupt a silent or partial line and select does not cover Windows pipes, so expiry kills the server; an expired session is unusable by design stdio_capture.py[CG5T]: F:Drains an MCP test server's stderr on a daemon thread, retains a bounded 16 KiB tail, and appends it to early-EOF diagnostics | R:code:scripts/blackbox/mcp_conformance.py,code:scripts/blackbox/mcp_scenarios.py,code:scripts/blackbox/mcp_upgrade.py | A:BoundedStderr,stderr_failure | S:An unread stderr pipe fills the OS buffer and blocks both sides; failure joins briefly and tolerates a session without a collector so bare test construction stays valid diff --git a/internal/machinecontract/overview_session.go b/internal/machinecontract/overview_session.go new file mode 100644 index 00000000..f150a917 --- /dev/null +++ b/internal/machinecontract/overview_session.go @@ -0,0 +1,11 @@ +package machinecontract + +// Runtime resource limits, not persisted project policy or cognition identity. +const ( + OverviewDeliverySessionPrefix = "v2" + OverviewDeliverySessionMaxCount = 32 + OverviewDeliverySessionMaxBytes = 64 << 20 + OverviewDeliverySessionIdleMillis = 15 * 60 * 1000 + OverviewDeliverySessionLifetimeMillis = 60 * 60 * 1000 + OverviewDeliveryCursorMaxBytes = 512 +) diff --git a/internal/managedstate/state.go b/internal/managedstate/state.go index c14850a4..220dd1ea 100644 --- a/internal/managedstate/state.go +++ b/internal/managedstate/state.go @@ -12,11 +12,13 @@ import ( "github.com/aoci-spec/aoci-code/internal/cognitionbudget" "github.com/aoci-spec/aoci-code/internal/config" "github.com/aoci-spec/aoci-code/internal/curation" + afs "github.com/aoci-spec/aoci-code/internal/fs" "github.com/aoci-spec/aoci-code/internal/index" "github.com/aoci-spec/aoci-code/internal/managedscope" ) type State struct { + Inventory *afs.SafeInventory `json:"-"` // Actual Legacy selection, not a second scan. Legacy bool `json:"legacy"` ScopeChangeRequired bool `json:"scope_change_required"` PolicyAligned bool `json:"policy_aligned"` @@ -42,7 +44,7 @@ func Load(root string, cfg *config.Config) (*State, error) { state := &State{Snapshot: map[string]baseline.Fingerprint{}, Baseline: value, Warnings: []string{}} if cfg.ManagedScope == nil && cfg.CognitionBudget == nil && (!exists || value.ManagedScope == nil) { state.Legacy, state.PolicyAligned, state.BudgetAligned = true, true, true - state.Snapshot, state.Warnings, err = baseline.Snapshot(root, cfg.WalkOptions()) + state.Snapshot, state.Warnings, state.Inventory, err = baseline.SnapshotWithInventory(root, cfg.WalkOptions()) return state, err } curationExclude, err := CurationExclusions(root, cfg, value) diff --git a/internal/mcptools/overview_delivery_v1.go b/internal/mcptools/overview_delivery_v1.go index 55da9bbc..5c7e7b5d 100644 --- a/internal/mcptools/overview_delivery_v1.go +++ b/internal/mcptools/overview_delivery_v1.go @@ -167,6 +167,25 @@ type overviewRenderContext struct { // Attestation per delivered body so the two may arrive in separate calls // in either order; nil keeps rendering a pure function of the input. Session *cognitionRefreshSession + // Prepared is immutable. Pure render callers may omit it; a delivery session + // supplies it to reuse the exact frame, canonical sequence and chunk plan. + Prepared *preparedOverview + LedgerEnabled bool +} + +type preparedOverview struct { + body framedOverviewBody + challenge overviewChallenge + spans []overviewChunkSpan +} + +func prepareOverview(ctx overviewRenderContext, tokens int) (*preparedOverview, error) { + body := frameOverviewBody(ctx.Root, ctx.EffectiveScope, ctx.ScopeIdentity, ctx.EntryCount, ctx.Content) + spans, err := planOverviewChunks(body.Text, tokens, len(body.Receipt.StartMarker)+1, ctx.Sequence) + if err != nil { + return nil, err + } + return &preparedOverview{body: body, challenge: buildOverviewChallenge(ctx.ScopeIdentity, ctx.Sequence), spans: spans}, nil } type overviewDeliveryFacts struct { @@ -198,8 +217,14 @@ func renderOverviewDelivery(ctx overviewRenderContext, input overviewIn, chunkTo if len(ctx.Sequence) != ctx.EntryCount { return overviewRendered{}, fmt.Errorf("overview_cognition_sequence_count_mismatch") } - challenge := buildOverviewChallenge(ctx.ScopeIdentity, ctx.Sequence) - framed := frameOverviewBody(ctx.Root, ctx.EffectiveScope, ctx.ScopeIdentity, ctx.EntryCount, ctx.Content) + var challenge overviewChallenge + var framed framedOverviewBody + if ctx.Prepared != nil { + challenge, framed = ctx.Prepared.challenge, ctx.Prepared.body + } else { + challenge = buildOverviewChallenge(ctx.ScopeIdentity, ctx.Sequence) + framed = frameOverviewBody(ctx.Root, ctx.EffectiveScope, ctx.ScopeIdentity, ctx.EntryCount, ctx.Content) + } hostStatus := hostDeliveryStatus(input.HostConfirmation, framed) report := input.ModelAttestation if input.HostConfirmation == nil && input.ModelAttestation == nil && input.Cursor == "" { @@ -210,10 +235,16 @@ func renderOverviewDelivery(ctx overviewRenderContext, input overviewIn, chunkTo // The two proof halves each bind this exact body, so the half missing from // this call may be the one an earlier call already delivered. hostStatus, report = ctx.Session.mergeDeliveryEvidence(framed.Receipt.BodySHA256, hostStatus, report) + if report != nil { + ledger.Append(ctx.Root, ctx.LedgerEnabled, ledger.Event{Op: "overview_attestation_scoring_started", Source: ledger.SourceAgent, IndexSHA256: ctx.ScopeIdentity, Result: "started"}) + } attestation := assessOverviewAttestation( challenge, ctx.EntryCount, ctx.EstimatedTokens, hostStatus, ctx.Assessment.Semantic.GovernanceAligned, report, ) + if report != nil { + ledger.Append(ctx.Root, ctx.LedgerEnabled, ledger.Event{Op: "overview_attestation_scored", Source: ledger.SourceAgent, IndexSHA256: ctx.ScopeIdentity, Result: attestation.ModelAttestation}) + } facts := overviewDeliveryFacts{ IndexSHA256: ctx.ScopeIdentity, IndexBytes: ctx.ContentBytes, EstimatedTokens: ctx.EstimatedTokens, SectionCount: ctx.SectionCount, @@ -402,9 +433,15 @@ func renderOverviewChunk( includeCognitionStateV2 bool, ) (string, error) { contentStart := len(body.Receipt.StartMarker) + 1 - spans, err := planOverviewChunks(body.Text, chunkTokens, contentStart, ctx.Sequence) - if err != nil { - return "", err + var spans []overviewChunkSpan + if ctx.Prepared != nil { + spans = ctx.Prepared.spans + } else { + var err error + spans, err = planOverviewChunks(body.Text, chunkTokens, contentStart, ctx.Sequence) + if err != nil { + return "", err + } } chunkIndex := 0 previousSHA := "" diff --git a/internal/mcptools/overview_diagnostics.go b/internal/mcptools/overview_diagnostics.go new file mode 100644 index 00000000..32232cb7 --- /dev/null +++ b/internal/mcptools/overview_diagnostics.go @@ -0,0 +1,46 @@ +package mcptools + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/aoci-spec/aoci-code/internal/ledger" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// The SDK validates arguments before the typed handler. This receiving +// boundary observes its schema errors without running the semantic scorer. +func overviewSchemaDiagnostics(root string, enabled bool) mcp.Middleware { + return func(next mcp.MethodHandler) mcp.MethodHandler { + return func(ctx context.Context, method string, request mcp.Request) (mcp.Result, error) { + result, err := next(ctx, method, request) + params, ok := request.GetParams().(*mcp.CallToolParamsRaw) + if !ok || method != "tools/call" || params.Name != "aoci_overview" { + return result, err + } + var fields map[string]json.RawMessage + if json.Unmarshal(params.Arguments, &fields) != nil || fields["model_cognition_attestation"] == nil { + return result, err + } + call, ok := result.(*mcp.CallToolResult) + if !ok || call.GetError() == nil || !strings.HasPrefix(call.GetError().Error(), "validating \"arguments\":") { + return result, err + } + call.Content = nil + call.SetError(fmt.Errorf("%s; %s", call.GetError(), mcpMessage("overview.attestation.schema_expected"))) + ledger.Append(root, enabled, ledger.Event{Op: "overview_attestation_schema_rejected", Source: ledger.SourceAgent, Result: "schema_rejected"}) + return result, err + } + } +} + +func recordOverviewProofEvents(root string, enabled bool, input overviewIn, delivery overviewRendered, body framedOverviewBody) { + if delivery.Facts.DeliveryMode == overviewDeliveryFull || (delivery.Facts.DeliveryMode == overviewDeliveryChunked && strings.HasSuffix(delivery.Output, body.Receipt.EndMarker+"\n")) { + ledger.Append(root, enabled, ledger.Event{Op: "overview_delivery_completed", Source: ledger.SourceAgent, IndexSHA256: delivery.Facts.IndexSHA256, Result: "completed"}) + } + if input.HostConfirmation != nil && hostDeliveryStatus(input.HostConfirmation, body) == hostDeliveryConfirmed { + ledger.Append(root, enabled, ledger.Event{Op: "overview_host_confirmation_accepted", Source: ledger.SourceAgent, IndexSHA256: delivery.Facts.IndexSHA256, Result: "accepted"}) + } +} diff --git a/internal/mcptools/overview_diagnostics_test.go b/internal/mcptools/overview_diagnostics_test.go new file mode 100644 index 00000000..cb9162e0 --- /dev/null +++ b/internal/mcptools/overview_diagnostics_test.go @@ -0,0 +1,116 @@ +package mcptools + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/aoci-spec/aoci-code/internal/config" + "github.com/aoci-spec/aoci-code/internal/ledger" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func overviewProofEvents(t *testing.T, root string) []ledger.Event { + t.Helper() + data, err := os.ReadFile(filepath.Join(root, ".aoci/ledger.jsonl")) + if err != nil { + t.Fatal(err) + } + var events []ledger.Event + for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { + var event ledger.Event + if err := json.Unmarshal([]byte(line), &event); err != nil { + t.Fatal(err) + } + if strings.HasPrefix(event.Op, "overview_") { + events = append(events, event) + } + } + return events +} + +func TestAttestationSchemaBoundaryDiagnosticsAndScoringStates(t *testing.T) { + for _, key := range []string{"object", "object_ref", "object_identity"} { + t.Run(key, func(t *testing.T) { + root := buildObservedVolumeRepo(t) + cfg, err := config.LoadBase(root) + if err != nil { + t.Fatal(err) + } + cfg.LedgerEnabled = true + if err := config.Save(root, cfg); err != nil { + t.Fatal(err) + } + session := connectMCPClient(t, root) + first := callVolumeTool(t, session, "aoci_overview", map[string]any{}) + args := volumeAttestationArguments(t, root, first, false) + report := args["model_cognition_attestation"].(map[string]any) + if key != "object_identity" { + for _, value := range report["challenge_answers"].([]any) { + answer := value.(map[string]any) + answer[key] = answer["object_identity"] + delete(answer, "object_identity") + } + } + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: "aoci_overview", Arguments: args}) + if err != nil { + t.Fatal(err) + } + events := overviewProofEvents(t, root) + ops := map[string]int{} + for _, event := range events { + ops[event.Op+":"+event.Result]++ + } + if ops["overview_delivery_completed:completed"] != 1 { + t.Fatal("delivery completion not distinct") + } + if key != "object_identity" { + if !result.IsError || !strings.Contains(resText(t, result), "object_identity") || !strings.Contains(resText(t, result), "additionalProperties=false") { + t.Fatalf("missing precise schema diagnostic: %s", resText(t, result)) + } + if ops["overview_attestation_schema_rejected:schema_rejected"] != 1 || ops["overview_attestation_scoring_started:started"] != 0 || ops["overview_attestation_schema_accepted:accepted"] != 0 || ops["overview_attestation_scored:fail"] != 0 { + t.Fatalf("schema error reached scorer: %v", events) + } + if strings.Contains(resText(t, result), "cognition_usable") || strings.Contains(resText(t, result), "model_attestation: fail") { + t.Fatal("schema rejection claimed semantic cognition state") + } + } else { + if result.IsError || !strings.Contains(resText(t, result), "model_attestation: pass") { + t.Fatalf("correct schema failed: %s", resText(t, result)) + } + for _, op := range []string{"overview_host_confirmation_accepted:accepted", "overview_attestation_schema_accepted:accepted", "overview_attestation_scoring_started:started", "overview_attestation_scored:pass"} { + if ops[op] != 1 { + t.Fatalf("missing %s: %v", op, events) + } + } + // A schema-valid identity error reaches the unchanged semantic scorer. + for _, value := range report["challenge_answers"].([]any) { + value.(map[string]any)["object_identity"] = "code:wrong.go" + } + result, err = session.CallTool(context.Background(), &mcp.CallToolParams{Name: "aoci_overview", Arguments: args}) + if err != nil { + t.Fatal(err) + } + if result.IsError || !strings.Contains(resText(t, result), "model_attestation: fail") { + t.Fatal("semantic failure not reported distinctly") + } + events = overviewProofEvents(t, root) + if events[len(events)-2].Op == "overview_attestation_schema_rejected" { + t.Fatal("semantic failure misclassified") + } + found := false + for _, event := range events { + if event.Op == "overview_attestation_scored" && event.Result == "fail" { + found = true + } + } + if !found { + t.Fatal("semantic failure event missing") + } + } + }) + } +} diff --git a/internal/mcptools/overview_session_v2.go b/internal/mcptools/overview_session_v2.go new file mode 100644 index 00000000..5d51adeb --- /dev/null +++ b/internal/mcptools/overview_session_v2.go @@ -0,0 +1,328 @@ +package mcptools + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "sync" + "time" + "weak" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "github.com/aoci-spec/aoci-code/internal/ledger" + "github.com/aoci-spec/aoci-code/internal/machinecontract" +) + +// The store belongs to one MCP server, not a repository-global cache. Each +// delivery owns immutable bytes/guards and private split-proof evidence. Chunk +// position is encoded and authenticated in a token, never a mutable counter. +type volumeDeliveryStore struct { + mu sync.Mutex + entries map[string]*volumeDelivery + bytes int + now func() time.Time + maxCount, maxBytes int +} + +// Weak object identity prevents address reuse without retaining closed SDK +// sessions or their transport buffers. Equality survives referent reclamation. +// Direct callers use a separate label namespace only for in-process tests. +type volumeDeliveryOwner struct { + session weak.Pointer[mcp.ServerSession] + direct string +} + +type volumeDelivery struct { + id string + owner volumeDeliveryOwner + secret [32]byte + ctx overviewRenderContext + guard *overviewSourceGuard + semantic semanticChangeFacts + governanceIdentity string + tokens, bytes int + proofBytes int // protected by store.mu; high-water retained proof accounting + created, touched time.Time + complete bool // protected by store.mu; no chunk position is retained +} + +func newVolumeDeliveryStore() *volumeDeliveryStore { + return &volumeDeliveryStore{entries: map[string]*volumeDelivery{}, now: time.Now, maxCount: machinecontract.OverviewDeliverySessionMaxCount, maxBytes: machinecontract.OverviewDeliverySessionMaxBytes} +} + +func (store *volumeDeliveryStore) prune(now time.Time) { + for id, entry := range store.entries { + if now.Sub(entry.touched).Milliseconds() > machinecontract.OverviewDeliverySessionIdleMillis || now.Sub(entry.created).Milliseconds() > machinecontract.OverviewDeliverySessionLifetimeMillis { + delete(store.entries, id) + store.bytes -= entry.bytes + } + } +} + +func (store *volumeDeliveryStore) put(entry *volumeDelivery) error { + store.mu.Lock() + defer store.mu.Unlock() + now := store.now() + store.prune(now) + if entry.bytes > store.maxBytes || store.maxCount < 1 { + return fmt.Errorf("overview_session_capacity_exceeded") + } + for len(store.entries) >= store.maxCount || store.bytes+entry.bytes > store.maxBytes { + var oldest *volumeDelivery + for _, candidate := range store.entries { + if oldest == nil || candidate.touched.Before(oldest.touched) { + oldest = candidate + } + } + delete(store.entries, oldest.id) + store.bytes -= oldest.bytes + } + entry.created, entry.touched = now, now + store.entries[entry.id] = entry + store.bytes += entry.bytes + return nil +} + +func (store *volumeDeliveryStore) lookup(id string, owner volumeDeliveryOwner) *volumeDelivery { + store.mu.Lock() + defer store.mu.Unlock() + now := store.now() + store.prune(now) + entry := store.entries[id] + if entry == nil || entry.owner != owner { + return nil + } + entry.touched = now + return entry +} + +func (store *volumeDeliveryStore) proof(owner volumeDeliveryOwner, identity, bodyHash string) *volumeDelivery { + store.mu.Lock() + defer store.mu.Unlock() + now := store.now() + store.prune(now) + var latest *volumeDelivery + for _, entry := range store.entries { + if entry.owner != owner || entry.ctx.ScopeIdentity != identity || !entry.complete { + continue + } + if bodyHash != "" && entry.ctx.Prepared.body.Receipt.BodySHA256 != bodyHash { + continue + } + if latest == nil || entry.created.After(latest.created) { + latest = entry + } + } + if latest != nil { + latest.touched = now + } + return latest +} + +func (store *volumeDeliveryStore) completed(entry *volumeDelivery) { + store.mu.Lock() + defer store.mu.Unlock() + if store.entries[entry.id] == entry { + entry.complete = true + } +} + +// Proof halves are retained too. Reserve their conservative high-water cost +// before merge; replacement never reduces the budget while requests may hold +// the prior report. In-flight request arguments are transient, not store state. +func (store *volumeDeliveryStore) reserveProof(entry *volumeDelivery, report *overviewModelAttestation) error { + if report == nil { + return nil + } + encoded, err := json.Marshal(report) + if err != nil { + return err + } + cost := 8*len(encoded) + 4096 + store.mu.Lock() + defer store.mu.Unlock() + store.prune(store.now()) + if store.entries[entry.id] != entry { + return fmt.Errorf("overview_session_unavailable_restart_required") + } + if cost <= entry.proofBytes { + return nil + } + delta := cost - entry.proofBytes + if entry.bytes+delta > store.maxBytes { + return fmt.Errorf("overview_session_capacity_exceeded") + } + for store.bytes+delta > store.maxBytes { + var oldest *volumeDelivery + for _, candidate := range store.entries { + if candidate != entry && (oldest == nil || candidate.touched.Before(oldest.touched)) { + oldest = candidate + } + } + delete(store.entries, oldest.id) + store.bytes -= oldest.bytes + } + entry.proofBytes = cost + entry.bytes += delta + store.bytes += delta + return nil +} + +func (store *volumeDeliveryStore) remove(entry *volumeDelivery) { + store.mu.Lock() + defer store.mu.Unlock() + if store.entries[entry.id] == entry { + delete(store.entries, entry.id) + store.bytes -= entry.bytes + } +} + +func newVolumeDelivery(owner volumeDeliveryOwner, ctx overviewRenderContext, guard *overviewSourceGuard, semantic semanticChangeFacts, identity string, tokens int) (*volumeDelivery, error) { + nonce := make([]byte, 16) + if _, err := rand.Read(nonce); err != nil { + return nil, err + } + entry := &volumeDelivery{id: hex.EncodeToString(nonce), owner: owner, ctx: ctx, guard: guard, semantic: semantic, governanceIdentity: identity, tokens: tokens} + if _, err := rand.Read(entry.secret[:]); err != nil { + return nil, err + } + // Conservative retained-memory accounting includes backing strings and maps; + // no whole inventory, source body, or managed-state evaluation is retained. + entry.bytes = 8*len(ctx.Content) + 8*len(ctx.Prepared.body.Text) + 2048*ctx.EntryCount + for path := range guard.sources { + entry.bytes += 512 + 4*len(path) + } + for path := range guard.metadata { + entry.bytes += 256 + 4*len(path) + } + entry.ctx.Session = newCognitionRefreshSession() + return entry, nil +} + +func (entry *volumeDelivery) cursor(legacy string) string { + prefix := machinecontract.OverviewDeliverySessionPrefix + ":" + entry.id + ":" + legacy + mac := hmac.New(sha256.New, entry.secret[:]) + _, _ = mac.Write([]byte(prefix)) + return prefix + ":" + hex.EncodeToString(mac.Sum(nil)) +} + +func (entry *volumeDelivery) decode(cursor string) (string, error) { + if len(cursor) > machinecontract.OverviewDeliveryCursorMaxBytes { + return "", fmt.Errorf("overview_cursor_invalid") + } + parts := strings.Split(cursor, ":") + if len(parts) != 7 || parts[0] != machinecontract.OverviewDeliverySessionPrefix || parts[1] != entry.id { + return "", fmt.Errorf("overview_cursor_invalid") + } + signature, err := hex.DecodeString(parts[6]) + if err != nil { + return "", fmt.Errorf("overview_cursor_invalid") + } + mac := hmac.New(sha256.New, entry.secret[:]) + _, _ = mac.Write([]byte(strings.Join(parts[:6], ":"))) + if !hmac.Equal(signature, mac.Sum(nil)) { + return "", fmt.Errorf("overview_cursor_chain_mismatch") + } + legacy := strings.Join(parts[2:6], ":") + if _, _, err := decodeOverviewCursor(legacy, entry.ctx.ScopeIdentity, entry.tokens); err != nil { + return "", err + } + return legacy, nil +} + +func rewriteVolumeCursor(delivery *overviewRendered, entry *volumeDelivery) error { + if delivery.Facts.DeliveryMode != overviewDeliveryChunked { + return nil + } + prefix, body, ok := strings.Cut(delivery.Output, "\n"+overviewChunkBodyMarker+"\n") + if !ok { + return fmt.Errorf("overview_session_frame_invalid") + } + var metadata map[string]any + if err := json.Unmarshal([]byte(prefix), &metadata); err != nil { + return err + } + if next, ok := metadata["next_cursor"].(string); ok && next != "" { + metadata["next_cursor"] = entry.cursor(next) + } + encoded, err := json.Marshal(metadata) + if err != nil { + return err + } + delivery.Output = string(encoded) + "\n" + overviewChunkBodyMarker + "\n" + body + delivery.Facts.OutputBytes = len(delivery.Output) + return nil +} + +type overviewWork struct { + GovernanceEvals int `json:"governance_evals"` + BodyBuilds int `json:"body_builds"` +} + +func overviewWorkResult(delivery overviewRendered, work overviewWork) *mcp.CallToolResult { + result := textResult(delivery.Output) + result.SetMeta(map[string]any{"aoci_work": work}) + return result +} + +func volumeOwner(request *mcp.CallToolRequest) volumeDeliveryOwner { + if request == nil || request.Session == nil { + return volumeDeliveryOwner{direct: "direct"} + } + return volumeDeliveryOwner{session: weak.Make(request.Session)} +} + +func deliveryCompleted(delivery overviewRendered, entry *volumeDelivery) bool { + if delivery.Facts.DeliveryMode == overviewDeliveryFull { + return true + } + if delivery.Facts.DeliveryMode != overviewDeliveryChunked { + return false + } + return strings.HasSuffix(delivery.Output, entry.ctx.Prepared.body.Receipt.EndMarker+"\n") +} + +// The token is bound to both the immutable snapshot and its Host session. +// Postflight repeats the cheap input guard, never the full policy evaluation. +func serveVolumeDelivery(root string, input overviewIn, loaded *cognitionRepoCtx, refresh *cognitionRefreshSession, entry *volumeDelivery, start time.Time) *mcp.CallToolResult { + if err := entry.guard.validate(root, loaded); err != nil { + refresh.deliveries.remove(entry) + return overviewSnapshotFailure() + } + if err := refresh.deliveries.reserveProof(entry, input.ModelAttestation); err != nil { + return errResult(errBadArgs, err.Error(), "") + } + hostReasons, eventID, fail := normalizeRefreshInput(input) + if fail != nil { + return failResult(fail) + } + refresh.mu.Lock() + ctx := entry.ctx + ctx.Assessment = refresh.evaluate(input, ctx.Receipt, entry.semantic, hostReasons, eventID) + delivery, err := renderOverviewDelivery(ctx, input, entry.tokens) + refresh.mu.Unlock() + if err != nil { + return errResult(errBadArgs, err.Error(), "") + } + if err := entry.guard.validate(root, loaded); err != nil { + refresh.deliveries.remove(entry) + return overviewSnapshotFailure() + } + if err := rewriteVolumeCursor(&delivery, entry); err != nil { + return errResult(errInternal, err.Error(), "") + } + if deliveryCompleted(delivery, entry) { + refresh.deliveries.completed(entry) + } + refresh.mu.Lock() + refresh.recordAttestedDelivery(ctx.Receipt, delivery.Attestation, entry.semantic.GovernanceAligned) + refresh.mu.Unlock() + ledger.Append(root, loaded.cfg.LedgerEnabled, delivery.Facts.ledgerEvent(time.Since(start).Milliseconds())) + recordOverviewProofEvents(root, loaded.cfg.LedgerEnabled, input, delivery, ctx.Prepared.body) + return overviewWorkResult(delivery, overviewWork{}) +} diff --git a/internal/mcptools/overview_session_v2_test.go b/internal/mcptools/overview_session_v2_test.go new file mode 100644 index 00000000..45cd11b3 --- /dev/null +++ b/internal/mcptools/overview_session_v2_test.go @@ -0,0 +1,574 @@ +package mcptools + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/aoci-spec/aoci-code/internal/config" + "github.com/aoci-spec/aoci-code/internal/machinecontract" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func chunkSessionRepo(t *testing.T) string { + t.Helper() + root := buildVolumeRepo(t, true, false) + names := make([]string, 120) + for i := range names { + names[i] = fmt.Sprintf("entry-%03d.go", i) + } + writeVolumeAttestationEntries(t, root, names) + return root +} + +func sessionOverview(t *testing.T, root string, session *cognitionRefreshSession, owner string, input overviewIn) *mcp.CallToolResult { + t.Helper() + loaded, fail := loadCognitionCtx(root) + if fail != nil { + return failResult(fail) + } + return handleVolumeOverview(root, "session-test", input, loaded, session, time.Now(), volumeDeliveryOwner{direct: owner}) +} + +func chunkMetadata(t *testing.T, result *mcp.CallToolResult) (map[string]any, string) { + t.Helper() + if result.IsError { + t.Fatalf("overview failed: %s", resText(t, result)) + } + prefix, body, ok := strings.Cut(resText(t, result), "\n"+overviewChunkBodyMarker+"\n") + if !ok { + t.Fatalf("missing chunk: %s", prefix) + } + var meta map[string]any + if err := json.Unmarshal([]byte(prefix), &meta); err != nil { + t.Fatal(err) + } + return meta, body +} + +func workCounters(t *testing.T, result *mcp.CallToolResult) overviewWork { + t.Helper() + data, err := json.Marshal(result.GetMeta()["aoci_work"]) + if err != nil { + t.Fatal(err) + } + var work overviewWork + if err := json.Unmarshal(data, &work); err != nil { + t.Fatal(err) + } + return work +} + +func TestOverviewV2CanonicalChunksAndWork(t *testing.T) { + root := chunkSessionRepo(t) + session := newCognitionRefreshSession() + loaded, fail := loadCognitionCtx(root) + if fail != nil { + t.Fatal(fail) + } + view, err := loaded.set.Scope("all") + if err != nil { + t.Fatal(err) + } + body, sequence, err := buildVolumeOverviewBody(view) + if err != nil { + t.Fatal(err) + } + ctx := overviewRenderContext{Root: root, EffectiveScope: view.EffectiveScope, ScopeIdentity: view.ScopeIdentity, Content: body, EntryCount: view.ObjectCount, Sequence: sequence} + expected := frameOverviewBody(root, view.EffectiveScope, view.ScopeIdentity, view.ObjectCount, body) + var canonical strings.Builder + cursor := "" + for ordinal := 1; ordinal < 20; ordinal++ { + result := sessionOverview(t, root, session, "host-a", overviewIn{Scope: "all", Cursor: cursor}) + meta, chunk := chunkMetadata(t, result) + want := overviewWork{} + if ordinal == 1 { + want = overviewWork{GovernanceEvals: 1, BodyBuilds: 1} + } + if ordinal == 1 && meta["body_sha256"] != expected.Receipt.BodySHA256 { + t.Fatal("body identity changed") + } + if work := workCounters(t, result); work != want { + t.Fatalf("chunk %d work=%+v want=%+v", ordinal, work, want) + } + if int(meta["chunk_index"].(float64)) != ordinal { + t.Fatal("chunk order changed") + } + legacy := cursor + if cursor != "" { + parts := strings.Split(cursor, ":") + legacy = strings.Join(parts[2:6], ":") + } + original, err := renderOverviewDelivery(ctx, overviewIn{Cursor: legacy}, loaded.cfg.OverviewDelivery.ChunkTokens) + if err != nil { + t.Fatal(err) + } + _, expectedChunk := chunkMetadata(t, textResult(original.Output)) + if chunk != expectedChunk { + t.Fatal("chunk bytes/boundaries changed") + } + canonical.WriteString(chunk) + if meta["completed"].(bool) { + if canonical.String() != expected.Text { + t.Fatal("canonical body changed") + } + return + } + cursor = meta["next_cursor"].(string) + if !strings.HasPrefix(cursor, "v2:") { + t.Fatal("not a session cursor") + } + } + t.Fatal("never completed") +} + +func TestOverviewV2RejectsInputDrift(t *testing.T) { + cases := map[string]func(*testing.T, string){ + "source same size same mtime": func(t *testing.T, root string) { + path := filepath.Join(root, "entry-000.go") + before, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + writeVolumeTestFile(t, root, "entry-000.go", "package changed\n") + if err := os.Chtimes(path, before.ModTime(), before.ModTime()); err != nil { + t.Fatal(err) + } + }, + "membership add": func(t *testing.T, root string) { writeVolumeTestFile(t, root, "fresh.go", "package fresh\n") }, + "membership remove": func(t *testing.T, root string) { + if err := os.Remove(filepath.Join(root, "entry-000.go")); err != nil { + t.Fatal(err) + } + }, + "index": func(t *testing.T, root string) { + p := filepath.Join(root, "aoci.code.txt") + b, err := os.ReadFile(p) + if err != nil { + t.Fatal(err) + } + writeVolumeTestFile(t, root, "aoci.code.txt", string(b)+"# drift\n") + }, + "config": func(t *testing.T, root string) { + cfg, err := config.LoadBase(root) + if err != nil { + t.Fatal(err) + } + cfg.CognitionRefreshThreshold++ + if err := config.Save(root, cfg); err != nil { + t.Fatal(err) + } + }, + "baseline": func(t *testing.T, root string) { + p := filepath.Join(root, ".aoci/baseline.json") + b, err := os.ReadFile(p) + if err != nil { + t.Fatal(err) + } + writeVolumeTestFile(t, root, ".aoci/baseline.json", string(b)+" \n") + }, + "recovery": func(t *testing.T, root string) { + writeVolumeTestFile(t, root, ".aoci/transactions/entries-pending.json", "{}\n") + }, + "atomic intent": func(t *testing.T, root string) { + writeVolumeTestFile(t, root, "aoci.code.txt.aoci-cas.intent", "pending") + }, + "curation": func(t *testing.T, root string) { writeVolumeTestFile(t, root, ".aoci/curation.json", "{}\n") }, + } + for name, mutate := range cases { + t.Run(name, func(t *testing.T) { + root := chunkSessionRepo(t) + session := newCognitionRefreshSession() + meta, _ := chunkMetadata(t, sessionOverview(t, root, session, "owner", overviewIn{})) + mutate(t, root) + result := sessionOverview(t, root, session, "owner", overviewIn{Cursor: meta["next_cursor"].(string)}) + if !result.IsError { + t.Fatal("stale continuation served") + } + }) + } +} + +func TestOverviewV2GuardBindsInitialSnapshotAndObserveBytes(t *testing.T) { + root := buildObservedVolumeRepo(t) + loaded, fail := loadCognitionCtx(root) + if fail != nil { + t.Fatal(fail) + } + metadata, err := deliveryMetadata(root, loaded) + if err != nil { + t.Fatal(err) + } + _, _, state, fail := inspectVolumeGovernanceWithSnapshot(root, loaded) + if fail != nil { + t.Fatal(fail) + } + guard := newOverviewSourceGuard(loaded, metadata, state) + if guard == nil || guard.validate(root, loaded) != nil { + t.Fatal("aligned snapshot not reusable") + } + writeVolumeTestFile(t, root, "notes.txt", "observe-only changed notes\n") + if guard.validate(root, loaded) == nil { + t.Fatal("observe drift between assessment and publish escaped") + } +} + +func TestOverviewV2IsolationIntegrityRestartAndEviction(t *testing.T) { + root := chunkSessionRepo(t) + session := newCognitionRefreshSession() + a, _ := chunkMetadata(t, sessionOverview(t, root, session, "a", overviewIn{})) + b, _ := chunkMetadata(t, sessionOverview(t, root, session, "b", overviewIn{Scope: "code"})) + cursor := a["next_cursor"].(string) + for name, input := range map[string]overviewIn{"foreign scope": {Scope: "code", Cursor: cursor}, "modified": {Cursor: cursor[:len(cursor)-1] + "x"}} { + if !sessionOverview(t, root, session, "a", input).IsError { + t.Fatal(name + " accepted") + } + } + if !sessionOverview(t, root, session, "b", overviewIn{Cursor: cursor}).IsError { + t.Fatal("foreign owner accepted") + } + if !sessionOverview(t, root, newCognitionRefreshSession(), "a", overviewIn{Cursor: cursor}).IsError { + t.Fatal("restart accepted ephemeral token") + } + var wg sync.WaitGroup + for _, item := range []struct{ owner, scope, cursor string }{{"a", "all", cursor}, {"b", "code", b["next_cursor"].(string)}} { + wg.Add(1) + go func(owner, scope, cursor string) { + defer wg.Done() + result := sessionOverview(t, root, session, owner, overviewIn{Scope: scope, Cursor: cursor}) + meta, _ := chunkMetadata(t, result) + if meta["chunk_index"].(float64) != 2 { + t.Error("position contaminated") + } + }(item.owner, item.scope, item.cursor) + } + wg.Wait() + // No state reconstruction for eviction; a caller must start fresh. + session.deliveries.maxCount = 1 + chunkMetadata(t, sessionOverview(t, root, session, "c", overviewIn{})) + if !sessionOverview(t, root, session, "a", overviewIn{Cursor: cursor}).IsError { + t.Fatal("evicted session accepted") + } + // V1 continues to use the documented reconstructable live-validation path. + parts := strings.Split(cursor, ":") + legacy := strings.Join(parts[2:6], ":") + result := sessionOverview(t, root, newCognitionRefreshSession(), "a", overviewIn{Cursor: legacy}) + chunkMetadata(t, result) + if work := workCounters(t, result); work != (overviewWork{GovernanceEvals: 2, BodyBuilds: 1}) { + t.Fatalf("legacy path changed: %+v", work) + } +} + +func TestOverviewV2StoreBoundsAndExpiry(t *testing.T) { + store := newVolumeDeliveryStore() + now := time.Unix(1, 0) + store.now = func() time.Time { return now } + store.maxCount = 2 + store.maxBytes = 20 + a := &volumeDelivery{id: "a", owner: volumeDeliveryOwner{direct: "owner"}, bytes: 11} + b := &volumeDelivery{id: "b", owner: volumeDeliveryOwner{direct: "owner"}, bytes: 11} + if store.put(a) != nil || store.put(b) != nil { + t.Fatal("put failed") + } + if store.lookup("a", volumeDeliveryOwner{direct: "owner"}) != nil { + t.Fatal("byte cap not enforced") + } + if store.put(&volumeDelivery{id: "large", bytes: 21}) == nil { + t.Fatal("oversize accepted") + } + now = now.Add(time.Duration(machinecontract.OverviewDeliverySessionIdleMillis+1) * time.Millisecond) + if store.lookup("b", volumeDeliveryOwner{direct: "owner"}) != nil || store.bytes != 0 { + t.Fatal("expired state retained") + } +} + +func TestOverviewV2MCPHostIsolation(t *testing.T) { + root := chunkSessionRepo(t) + server, err := newMCPServer(root, "test") + if err != nil { + t.Fatal(err) + } + connect := func() *mcp.ClientSession { + ct, st := mcp.NewInMemoryTransports() + ss, err := server.Connect(context.Background(), st, nil) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = ss.Close() }) + cs, err := mcp.NewClient(&mcp.Implementation{Name: "test", Version: "test"}, nil).Connect(context.Background(), ct, nil) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = cs.Close() }) + return cs + } + a, b := connect(), connect() + result, err := a.CallTool(context.Background(), &mcp.CallToolParams{Name: "aoci_overview", Arguments: map[string]any{}}) + if err != nil { + t.Fatal(err) + } + meta, _ := chunkMetadata(t, result) + foreign, err := b.CallTool(context.Background(), &mcp.CallToolParams{Name: "aoci_overview", Arguments: map[string]any{"cursor": meta["next_cursor"]}}) + if err != nil { + t.Fatal(err) + } + if !foreign.IsError { + t.Fatal("foreign SDK session accepted cursor") + } + if err := a.Close(); err != nil { + t.Fatal(err) + } + c := connect() + foreign, err = c.CallTool(context.Background(), &mcp.CallToolParams{Name: "aoci_overview", Arguments: map[string]any{"cursor": meta["next_cursor"]}}) + if err != nil { + t.Fatal(err) + } + if !foreign.IsError { + t.Fatal("replacement connection inherited closed Host delivery") + } +} + +func TestOverviewV2CompactionConsumesGenerationAfterPartialProof(t *testing.T) { + root := buildObservedVolumeRepo(t) + session := newCognitionRefreshSession() + result := sessionOverview(t, root, session, "host", overviewIn{Scope: "all", RefreshReasons: []string{"context_compaction"}, RefreshEventID: "compaction-test"}) + if result.IsError { + t.Fatal(resText(t, result)) + } + args := volumeAttestationArguments(t, root, resText(t, result), false) + report := args["model_cognition_attestation"].(map[string]any) + report["system_mastery_percent"] = 60 + encoded, err := json.Marshal(args) + if err != nil { + t.Fatal(err) + } + var input overviewIn + if err := json.Unmarshal(encoded, &input); err != nil { + t.Fatal(err) + } + result = sessionOverview(t, root, session, "host", input) + if result.IsError || !strings.Contains(resText(t, result), "cognition_assimilation: partial") { + t.Fatalf("partial proof failed: %s", resText(t, result)) + } + if !session.established || session.generation != 1 || len(session.pendingEvents) != 0 || len(session.pendingReasons) != 0 { + t.Fatalf("refresh generation not consumed: %+v", session) + } + if receiptReliable(session.lastReceipt) { + t.Fatal("partial proof upgraded reliable cognition") + } + if work := workCounters(t, result); work != (overviewWork{}) { + t.Fatalf("proof reran expensive work: %+v", work) + } +} + +func TestOverviewV2FallbackForDatabaseTimeDependentGovernance(t *testing.T) { + root := buildVolumeRepo(t, true, true) + result := sessionOverview(t, root, newCognitionRefreshSession(), "host", overviewIn{}) + if result.IsError { + t.Fatal(resText(t, result)) + } + if work := workCounters(t, result); work != (overviewWork{GovernanceEvals: 2, BodyBuilds: 1}) { + t.Fatalf("database bypassed live governance: %+v", work) + } +} + +func TestOverviewV2AbsoluteTTLAndCountBound(t *testing.T) { + store := newVolumeDeliveryStore() + now := time.Unix(1, 0) + store.now = func() time.Time { return now } + store.maxCount = 2 + store.maxBytes = 1000 + for _, id := range []string{"a", "b", "c"} { + if err := store.put(&volumeDelivery{id: id, owner: volumeDeliveryOwner{direct: "host"}, bytes: 1}); err != nil { + t.Fatal(err) + } + now = now.Add(time.Second) + } + if len(store.entries) != 2 || store.lookup("a", volumeDeliveryOwner{direct: "host"}) != nil { + t.Fatal("count cap failed") + } + for i := 0; i < 7; i++ { + now = now.Add(10 * time.Minute) + entry := store.lookup("c", volumeDeliveryOwner{direct: "host"}) + if i < 5 && entry == nil { + t.Fatal("premature expiry") + } + if i == 6 && entry != nil { + t.Fatal("active replay defeated lifetime cap") + } + } +} + +func TestOverviewV2CurrentMainInventoryMutations(t *testing.T) { + cases := map[string]func(*testing.T, string){ + "repository initialization": func(t *testing.T, root string) { runFixtureGit(t, root, "init", "-q") }, + "tracked status": func(t *testing.T, root string) { runFixtureGit(t, root, "add", "entry-000.go") }, + "ignored status": func(t *testing.T, root string) { writeVolumeTestFile(t, root, ".git/info/exclude", "entry-000.go\n") }, + "submodule boundary": func(t *testing.T, root string) { + runFixtureGit(t, root, "update-index", "--add", "--cacheinfo", "160000,1111111111111111111111111111111111111111,module") + }, + "unsafe boundary": func(t *testing.T, root string) { + if err := os.Symlink("entry-000.go", filepath.Join(root, "boundary.go")); err != nil { + t.Fatal(err) + } + }, + "root exclusion": func(t *testing.T, root string) { + cfg, err := config.LoadBase(root) + if err != nil { + t.Fatal(err) + } + cfg.ExcludeRootDirs = append(cfg.ExcludeRootDirs, "new-artifacts") + if err := config.Save(root, cfg); err != nil { + t.Fatal(err) + } + }, + } + for name, mutate := range cases { + t.Run(name, func(t *testing.T) { + root := chunkSessionRepo(t) + if name != "repository initialization" { + runFixtureGit(t, root, "init", "-q") + } + session := newCognitionRefreshSession() + meta, _ := chunkMetadata(t, sessionOverview(t, root, session, "host", overviewIn{})) + mutate(t, root) + if !sessionOverview(t, root, session, "host", overviewIn{Cursor: meta["next_cursor"].(string)}).IsError { + t.Fatal("stale inventory accepted") + } + }) + } +} + +func TestOverviewV2DuplicateContinuationIsImmutable(t *testing.T) { + root := chunkSessionRepo(t) + session := newCognitionRefreshSession() + meta, _ := chunkMetadata(t, sessionOverview(t, root, session, "host", overviewIn{})) + input := overviewIn{Cursor: meta["next_cursor"].(string)} + const clients = 8 + results := make(chan *mcp.CallToolResult, clients) + var wg sync.WaitGroup + for i := 0; i < clients; i++ { + wg.Add(1) + go func() { defer wg.Done(); results <- sessionOverview(t, root, session, "host", input) }() + } + wg.Wait() + close(results) + var body, next string + for result := range results { + metadata, chunk := chunkMetadata(t, result) + if metadata["chunk_index"].(float64) != 2 || workCounters(t, result) != (overviewWork{}) { + t.Fatal("duplicate consumed mutable position or rebuilt body") + } + if body == "" { + body, next = chunk, metadata["next_cursor"].(string) + } + if chunk != body || metadata["next_cursor"] != next { + t.Fatal("duplicate continuation differs") + } + } +} + +func TestOverviewV2RetainedProofBudget(t *testing.T) { + store := newVolumeDeliveryStore() + store.maxBytes = 10000 + a := &volumeDelivery{id: "a", bytes: 3000} + b := &volumeDelivery{id: "b", bytes: 3000} + if store.put(a) != nil || store.put(b) != nil { + t.Fatal("setup failed") + } + report := &overviewModelAttestation{Version: "proof"} + if err := store.reserveProof(a, report); err != nil { + t.Fatal(err) + } + if len(store.entries) != 1 || store.entries[a.id] != a || a.proofBytes == 0 || store.bytes != a.bytes || store.bytes > store.maxBytes { + t.Fatal("proof growth bypassed budget or evicted its own delivery") + } + before := store.bytes + if err := store.reserveProof(a, report); err != nil || store.bytes != before { + t.Fatal("replacement did not preserve high-water accounting") + } + report.UncertaintyReasons = []string{strings.Repeat("x", 10000)} + if store.reserveProof(a, report) == nil || store.bytes != before { + t.Fatal("oversize proof accepted or corrupted accounting") + } + store.remove(a) + if store.bytes != 0 { + t.Fatal("proof bytes leaked after eviction") + } + if store.reserveProof(a, &overviewModelAttestation{}) == nil { + t.Fatal("evicted delivery accepted retained proof") + } +} + +func TestOverviewV2ProofActivityHonorsIdleAndAbsoluteTTL(t *testing.T) { + store := newVolumeDeliveryStore() + now := time.Unix(1, 0) + store.now = func() time.Time { return now } + owner := volumeDeliveryOwner{direct: "host"} + entry := &volumeDelivery{id: "active-proof", owner: owner, bytes: 1, ctx: overviewRenderContext{ScopeIdentity: "scope"}} + if err := store.put(entry); err != nil { + t.Fatal(err) + } + store.completed(entry) + for i := 0; i < 7; i++ { + now = now.Add(10 * time.Minute) + got := store.proof(owner, "scope", "") + if i < 6 && got != entry { + t.Fatal("active proof lookup failed idle TTL") + } + if i == 6 && got != nil { + t.Fatal("proof lookup defeated absolute TTL") + } + } +} + +func TestOverviewV2ManagedExistingUnsafeBoundaries(t *testing.T) { + for _, kind := range []string{"symlink", "submodule"} { + t.Run(kind, func(t *testing.T) { + root := buildObservedVolumeRepo(t) + runFixtureGit(t, root, "init", "-q") + if kind == "symlink" { + if err := os.Symlink("main.go", filepath.Join(root, "boundary.go")); err != nil { + t.Fatal(err) + } + runFixtureGit(t, root, "add", "boundary.go") + } else { + runFixtureGit(t, root, "update-index", "--add", "--cacheinfo", "160000,1111111111111111111111111111111111111111,module") + } + loaded, fail := loadCognitionCtx(root) + if fail != nil { + t.Fatal(fail) + } + metadata, err := deliveryMetadata(root, loaded) + if err != nil { + t.Fatal(err) + } + _, _, state, fail := inspectVolumeGovernanceWithSnapshot(root, loaded) + if fail != nil { + t.Fatal(fail) + } + if state.Legacy { + t.Fatal("test must cover Managed Scope") + } + guard := newOverviewSourceGuard(loaded, metadata, state) + if guard == nil { + t.Fatal("supported snapshot rejected") + } + if err := guard.validate(root, loaded); err != nil { + t.Fatalf("unchanged existing %s boundary rejected: %v", kind, err) + } + if err := os.Symlink("main.go", filepath.Join(root, "another-boundary.go")); err != nil { + t.Fatal(err) + } + if guard.validate(root, loaded) == nil { + t.Fatal("boundary drift accepted") + } + }) + } +} diff --git a/internal/mcptools/overview_source_guard.go b/internal/mcptools/overview_source_guard.go new file mode 100644 index 00000000..ae30a4c2 --- /dev/null +++ b/internal/mcptools/overview_source_guard.go @@ -0,0 +1,235 @@ +package mcptools + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "sort" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "github.com/aoci-spec/aoci-code/internal/baseline" + "github.com/aoci-spec/aoci-code/internal/cognition" + "github.com/aoci-spec/aoci-code/internal/config" + afs "github.com/aoci-spec/aoci-code/internal/fs" + "github.com/aoci-spec/aoci-code/internal/machinecontract" + "github.com/aoci-spec/aoci-code/internal/managedscope" + "github.com/aoci-spec/aoci-code/internal/managedstate" +) + +// A guard binds the inputs, not merely the previous governance verdict. Every +// validation enumerates current Safe Inventory and hashes the governed bytes. +// It never re-evaluates policy against every excluded path, trusts mtime, or +// opens excluded business content. Index/observe roles are immutable only while +// config, curation, inventory membership/status and Baseline remain identical. +type overviewSourceGuard struct { + composite string + configIdentity string + metadata map[string]string + inventory string + legacy bool + sources map[string]baseline.Fingerprint +} + +func overviewConfigIdentity(cfg *config.Config) string { + encoded, _ := json.Marshal(cfg) + digest := sha256.Sum256(encoded) + return hex.EncodeToString(digest[:]) +} + +func deliveryMetadata(root string, loaded *cognitionRepoCtx) (map[string]string, error) { + result := map[string]string{} + paths := []string{loaded.paths.ConfigPath, config.LocalFilePath(root), loaded.paths.BaselinePath, loaded.paths.CurationPath} + for _, path := range paths { + digest, err := deliveryFileHash(path, true) + if err != nil { + return nil, err + } + result[path] = digest + } + + return result, nil +} + +func deliveryFileHash(path string, allowAbsent bool) (string, error) { + info, err := os.Lstat(path) + if allowAbsent && os.IsNotExist(err) { + return "absent", nil + } + if err != nil { + return "", err + } + if !info.Mode().IsRegular() { + return "", fmt.Errorf("overview_guard_nonregular") + } + file, err := os.Open(path) + if err != nil { + return "", err + } + defer file.Close() + current, err := file.Stat() + if err != nil || !os.SameFile(info, current) { + return "", fmt.Errorf("overview_guard_file_replaced") + } + digest := sha256.New() + if _, err := io.Copy(digest, file); err != nil { + return "", err + } + return hex.EncodeToString(digest.Sum(nil)), nil +} + +func overviewInventoryDigest(paths map[string]string, rules string, blockers int, topology string) string { + keys := make([]string, 0, len(paths)) + for path := range paths { + keys = append(keys, path) + } + sort.Strings(keys) + digest := sha256.New() + fmt.Fprintf(digest, "%s\x00%d\x00%s\x00", rules, blockers, topology) + for _, path := range keys { + fmt.Fprintf(digest, "%s\x00%s\x00", path, paths[path]) + } + return hex.EncodeToString(digest.Sum(nil)) +} + +// Repository boundaries are inventory facts, never submodule content or HEAD. +// Runtime exclusions may grow as this delivery writes its own Ledger. +func overviewInventoryTopology(repository bool, boundaries []string) string { + sort.Strings(boundaries) + encoded, _ := json.Marshal(struct { + Repository bool + Boundaries []string + }{repository, boundaries}) + digest := sha256.Sum256(encoded) + return hex.EncodeToString(digest[:]) +} + +func overviewSafeTopology(inventory *afs.SafeInventory) string { + boundaries := []string{} + for _, item := range inventory.Exclusions { + if item.Category == afs.SafetyUnsafe { + boundaries = append(boundaries, fmt.Sprintf("%s\x00%t\x00%s:%s", item.PathSummary, item.GitTracked, item.RuleSource, item.Category)) + } + } + return overviewInventoryTopology(inventory.Summary.GitRepository, boundaries) +} + +func overviewInventoryPaths(inventory *afs.SafeInventory) map[string]string { + tracked, ignored := map[string]bool{}, map[string]bool{} + for _, path := range inventory.TrackedPaths { + tracked[path] = true + } + for _, path := range inventory.IgnoredPaths { + ignored[path] = true + } + paths := map[string]string{} + for _, path := range inventory.ManagedCandidates { + status := "untracked_or_non_git" + if tracked[path] { + status = "tracked" + } + if ignored[path] { + status = "ignored" + } + paths[path] = status + } + return paths +} + +func newOverviewSourceGuard(loaded *cognitionRepoCtx, metadata map[string]string, state *managedstate.State) *overviewSourceGuard { + if state == nil || state.ScopeChangeRequired || len(state.Warnings) != 0 { + return nil + } + // Database Evidence can expire without byte drift. Until its time/connector + // identities have a reusable guard, retain the original live-governance path. + if asset := loaded.set.Volumes[cognition.ScopeDatabase]; asset != nil && asset.Descriptor.State != machinecontract.CognitionVolumeDisabled { + return nil + } + guard := &overviewSourceGuard{composite: loaded.set.CompositeIdentity, configIdentity: overviewConfigIdentity(loaded.cfg), metadata: metadata, legacy: state.Legacy, sources: map[string]baseline.Fingerprint{}} + candidates := map[string]string{} + for path, value := range state.Snapshot { + guard.sources[path] = value + + } + rules, blockers, topology := "", 0, "" + if state.Legacy { + if state.Inventory == nil { + return nil + } + candidates = overviewInventoryPaths(state.Inventory) + topology = overviewSafeTopology(state.Inventory) + rules, blockers = state.Inventory.Summary.RulesIdentity, state.Inventory.Summary.AutoBlockerCount + } else { + if state.Evaluation == nil { + return nil + } + rules, blockers = state.Evaluation.SafeInventory.RulesIdentity, state.Evaluation.SafeInventory.AutoBlockerCount + boundaries := []string{} + for _, item := range state.Evaluation.Exclude { + if item.RuleSource == machinecontract.ScopeRuleSafety && item.SafetyStatus == afs.SafetyUnsafe { + boundaries = append(boundaries, fmt.Sprintf("%s\x00%t\x00%s", item.Path, item.GitStatus == "tracked", item.Reason)) + } + } + topology = overviewInventoryTopology(state.Evaluation.SafeInventory.GitRepository, boundaries) + for _, group := range [][]managedscope.PathEvaluation{state.Evaluation.Index, state.Evaluation.Observe, state.Evaluation.Exclude} { + for _, item := range group { + if item.RuleSource != machinecontract.ScopeRuleSafety { + candidates[item.Path] = item.GitStatus + } + } + } + } + guard.inventory = overviewInventoryDigest(candidates, rules, blockers, topology) + return guard +} + +func (guard *overviewSourceGuard) validate(root string, loaded *cognitionRepoCtx) error { + if loaded.set.CompositeIdentity != guard.composite || overviewConfigIdentity(loaded.cfg) != guard.configIdentity { + return fmt.Errorf("overview_snapshot_changed") + } + metadata, err := deliveryMetadata(root, loaded) + if err != nil { + return err + } + before, _ := json.Marshal(guard.metadata) + after, _ := json.Marshal(metadata) + if string(before) != string(after) { + return fmt.Errorf("overview_snapshot_changed") + } + if fail := confirmCognitionSnapshot(root, loaded.set); fail != nil { + return fmt.Errorf("overview_snapshot_changed") + } + if fail := pendingCognitionDeliveryFail(root, loaded.set); fail != nil { + return fmt.Errorf("overview_snapshot_changed") + } + options := loaded.cfg.WalkOptions() + options.IncludeIgnoredCandidates = !guard.legacy + inventory, err := afs.BuildSafeInventory(root, options) + if err != nil { + return err + } + candidates := overviewInventoryPaths(inventory) + if overviewInventoryDigest(candidates, inventory.Summary.RulesIdentity, inventory.Summary.AutoBlockerCount, overviewSafeTopology(inventory)) != guard.inventory { + return fmt.Errorf("overview_snapshot_changed") + } + for path, expected := range guard.sources { + full := filepath.Join(root, filepath.FromSlash(path)) + info, err := os.Lstat(full) + if err != nil || !info.Mode().IsRegular() || info.Size() != expected.Size { + return fmt.Errorf("overview_snapshot_changed") + } + actual, err := deliveryFileHash(full, false) + if err != nil || actual != expected.SHA256 { + return fmt.Errorf("overview_snapshot_changed") + } + } + return nil +} + +func overviewSnapshotFailure() *mcp.CallToolResult { + return errResult(errCognitionSnapshotUnavailable, "overview_snapshot_changed", mcpMessage("overview.delivery.snapshot_retry")) +} diff --git a/internal/mcptools/overview_volume_governance.go b/internal/mcptools/overview_volume_governance.go index edc76e8b..f5dfb741 100644 --- a/internal/mcptools/overview_volume_governance.go +++ b/internal/mcptools/overview_volume_governance.go @@ -10,6 +10,7 @@ import ( "github.com/aoci-spec/aoci-code/internal/baseline" "github.com/aoci-spec/aoci-code/internal/machinecontract" + "github.com/aoci-spec/aoci-code/internal/managedstate" "github.com/aoci-spec/aoci-code/internal/volumegovernance" ) @@ -23,9 +24,14 @@ func inspectVolumeGovernance( root string, loaded *cognitionRepoCtx, ) (semanticChangeFacts, string, *Fail) { - facts, err := volumegovernance.Assess(root, loaded.cfg, loaded.set) + semantic, identity, _, fail := inspectVolumeGovernanceWithSnapshot(root, loaded) + return semantic, identity, fail +} + +func inspectVolumeGovernanceWithSnapshot(root string, loaded *cognitionRepoCtx) (semanticChangeFacts, string, *managedstate.State, *Fail) { + facts, snapshot, err := volumegovernance.AssessWithSnapshot(root, loaded.cfg, loaded.set) if err != nil { - return semanticChangeFacts{}, "", &Fail{ + return semanticChangeFacts{}, "", nil, &Fail{ Code: errInternal, Msg: mcpMessage("maintain.snapshot_failed", localeSafeMCPDetail(err.Error())), } @@ -37,7 +43,7 @@ func inspectVolumeGovernance( baselineIdentity = fingerprint.SHA256 case os.IsNotExist(hashErr): default: - return semanticChangeFacts{}, "", &Fail{ + return semanticChangeFacts{}, "", nil, &Fail{ Code: errInternal, Msg: mcpMessage("maintain.snapshot_failed", localeSafeMCPDetail(hashErr.Error())), } @@ -54,13 +60,13 @@ func inspectVolumeGovernance( BaselineSHA256 string `json:"baseline_sha256"` }{Facts: identityFacts, BaselineSHA256: baselineIdentity}) if err != nil { - return semanticChangeFacts{}, "", &Fail{ + return semanticChangeFacts{}, "", nil, &Fail{ Code: errInternal, Msg: mcpMessage("mcp.error.internal_recovered"), } } digest := sha256.Sum256(encoded) - return semanticFactsFromVolumeGovernance(loaded, facts), hex.EncodeToString(digest[:]), nil + return semanticFactsFromVolumeGovernance(loaded, facts), hex.EncodeToString(digest[:]), snapshot, nil } func semanticFactsFromVolumeGovernance( diff --git a/internal/mcptools/server.go b/internal/mcptools/server.go index fb46dffc..5bf11d30 100644 --- a/internal/mcptools/server.go +++ b/internal/mcptools/server.go @@ -8,7 +8,7 @@ // - 九工具名固定(aoci_rules/aoci_overview/aoci_get_entries/aoci_search/ // aoci_update_entry/aoci_report/aoci_remove_entry/aoci_header/aoci_maintain), // 改名破坏 AGENTS.md 模板与实验可复现性; -// - 每次工具调用现读 index/baseline/config,不做进程内缓存 —— 索引会被 MCP 外通道 +// - 每次工具调用现读 index/baseline/config;V2 续传只复用经实时守卫确认的不可变交付 —— 索引会被 MCP 外通道 // (编辑器手改/git pull)修改,缓存视图违反「过期索引比没有更危险」第一戒律; // - 参数全 string/[]string,返回纯文本; // - handler 内 panic 经 guard 恢复: stderr 记录 + 返回 MCP error 结果,不崩进程; @@ -290,6 +290,7 @@ func newMCPServer(root, version string) (*mcp.Server, error) { return nil, err } srv := mcp.NewServer(&mcp.Implementation{Name: machinecontract.MCPServerName, Version: version}, nil) + srv.AddReceivingMiddleware(overviewSchemaDiagnostics(root, cfg.LedgerEnabled)) refreshSession := newCognitionRefreshSession() registerReadTools(srv, root, version, descriptions, inputSchemas, refreshSession) registerWriteTools(srv, root, version, descriptions, inputSchemas, refreshSession) diff --git a/internal/mcptools/tools_read.go b/internal/mcptools/tools_read.go index 5ce0e375..bd41162a 100644 --- a/internal/mcptools/tools_read.go +++ b/internal/mcptools/tools_read.go @@ -28,6 +28,7 @@ import ( "github.com/aoci-spec/aoci-code/internal/baseline" "github.com/aoci-spec/aoci-code/internal/cognition" + "github.com/aoci-spec/aoci-code/internal/config" afs "github.com/aoci-spec/aoci-code/internal/fs" "github.com/aoci-spec/aoci-code/internal/index" "github.com/aoci-spec/aoci-code/internal/ledger" @@ -170,6 +171,12 @@ func registerReadTools( return guard( func() *mcp.CallToolResult { start := time.Now() + if input.ModelAttestation != nil { + cfg, err := config.Load(root) + if err == nil { + ledger.Append(root, cfg.LedgerEnabled, ledger.Event{Op: "overview_attestation_schema_accepted", Source: ledger.SourceAgent, Result: "accepted"}) + } + } if err := validateOverviewCognitionStateVersion(input.CognitionStateVersion); err != nil { return errResult( errBadArgs, @@ -189,7 +196,7 @@ func registerReadTools( return failResult(fail) } if loaded.set.LayoutMode == cognition.LayoutVolumesV1 { - return handleVolumeOverview(root, mcpServiceVersion, input, loaded, refreshSession, start) + return handleVolumeOverview(root, mcpServiceVersion, input, loaded, refreshSession, start, volumeOwner(request)) } repository := loaded.legacyRepo() @@ -266,7 +273,7 @@ func registerReadTools( EntryCount: entryCount, SectionCount: sectionCount, EstimatedTokens: len(repository.text) / 3, Receipt: deliveredReceipt, Assessment: assessment, - Sequence: sequence, Session: refreshSession, + Sequence: sequence, Session: refreshSession, LedgerEnabled: loaded.cfg.LedgerEnabled, }, input, loaded.cfg.OverviewDelivery.ChunkTokens) if resourceErr != nil { return errResult(errBadArgs, resourceErr.Error(), "") @@ -287,6 +294,7 @@ func registerReadTools( ), ) + recordOverviewProofEvents(root, loaded.cfg.LedgerEnabled, input, delivery, frameOverviewBody(root, cognitionScopeRepositoryFull, currentReceipt.IndexSHA256, entryCount, repository.text)) return textResult(delivery.Output) }, ), nil, nil diff --git a/internal/mcptools/tools_read_refresh.go b/internal/mcptools/tools_read_refresh.go index 7dc403e2..d40c520c 100644 --- a/internal/mcptools/tools_read_refresh.go +++ b/internal/mcptools/tools_read_refresh.go @@ -20,7 +20,8 @@ import ( ) type cognitionRefreshSession struct { - mu sync.Mutex + mu sync.Mutex + deliveries *volumeDeliveryStore established bool generation int @@ -55,6 +56,7 @@ type overviewDeliveryEvidence struct { func newCognitionRefreshSession() *cognitionRefreshSession { return &cognitionRefreshSession{ + deliveries: newVolumeDeliveryStore(), pendingReasons: map[string]bool{}, pendingEvents: map[string]bool{}, consumedEvents: map[string]bool{}, diff --git a/internal/mcptools/tools_read_volumes.go b/internal/mcptools/tools_read_volumes.go index b1b2bbfc..beea9794 100644 --- a/internal/mcptools/tools_read_volumes.go +++ b/internal/mcptools/tools_read_volumes.go @@ -12,6 +12,7 @@ import ( "github.com/aoci-spec/aoci-code/internal/cognition" "github.com/aoci-spec/aoci-code/internal/ledger" + "github.com/aoci-spec/aoci-code/internal/machinecontract" ) func handleVolumeOverview( @@ -20,6 +21,7 @@ func handleVolumeOverview( loaded *cognitionRepoCtx, refreshSession *cognitionRefreshSession, start time.Time, + owner volumeDeliveryOwner, ) *mcp.CallToolResult { view, err := loaded.set.Scope(input.Scope) if err != nil { @@ -30,10 +32,56 @@ func handleVolumeOverview( return failResult(fail) } current := newVolumeCognitionReceipt(root, mcpServiceVersion, loaded.set, view) - semantic, governanceIdentity, semanticFail := inspectVolumeGovernance(root, loaded) + // V2 is ephemeral and owner-bound. Never reconstruct an unknown V2 token. + if strings.HasPrefix(input.Cursor, "v2:") { + if input.CheckOnly { + return errResult(errBadArgs, "overview_cursor_requires_full_delivery", "") + } + if len(input.Cursor) > machinecontract.OverviewDeliveryCursorMaxBytes { + return errResult(errBadArgs, "overview_cursor_invalid", "") + } + parts := strings.Split(input.Cursor, ":") + if len(parts) != 7 { + return errResult(errBadArgs, "overview_cursor_invalid", "") + } + entry := refreshSession.deliveries.lookup(parts[1], owner) + if entry == nil { + return errResult(errBadArgs, "overview_session_unavailable_restart_required", "") + } + legacy, err := entry.decode(input.Cursor) + if err != nil { + return errResult(errBadArgs, err.Error(), "") + } + if entry.ctx.ScopeIdentity != view.ScopeIdentity || entry.ctx.RequestedScope != view.RequestedScope || entry.ctx.EffectiveScope != view.EffectiveScope || entry.tokens != loaded.cfg.OverviewDelivery.ChunkTokens { + return overviewSnapshotFailure() + } + input.Cursor = legacy + return serveVolumeDelivery(root, input, loaded, refreshSession, entry, start) + } + // Proof halves reuse only a complete delivery owned by this Host session. + if !input.CheckOnly && input.Cursor == "" && (input.HostConfirmation != nil || input.ModelAttestation != nil) { + hash := "" + if input.HostConfirmation != nil { + hash = input.HostConfirmation.BodySHA256 + } + if entry := refreshSession.deliveries.proof(owner, view.ScopeIdentity, hash); entry != nil && entry.ctx.RequestedScope == view.RequestedScope { + if entry.guard.validate(root, loaded) == nil { + return serveVolumeDelivery(root, input, loaded, refreshSession, entry, start) + } + // Live governance still scores an already changed proof as unaligned. + refreshSession.deliveries.remove(entry) + } + } + metadata, metadataErr := deliveryMetadata(root, loaded) + semantic, governanceIdentity, state, semanticFail := inspectVolumeGovernanceWithSnapshot(root, loaded) + work := overviewWork{GovernanceEvals: 1} if semanticFail != nil { return failResult(semanticFail) } + var guard *overviewSourceGuard + if metadataErr == nil && input.Cursor == "" && input.HostConfirmation == nil && input.ModelAttestation == nil && !input.CheckOnly { + guard = newOverviewSourceGuard(loaded, metadata, state) + } refreshSession.mu.Lock() defer refreshSession.mu.Unlock() @@ -82,6 +130,7 @@ func handleVolumeOverview( }) return textResult(output) } + work.BodyBuilds++ body, sequence, sequenceErr := buildVolumeOverviewBody(view) if sequenceErr != nil { return errResult(errInternal, sequenceErr.Error(), "") @@ -97,7 +146,7 @@ func handleVolumeOverview( current, assessment, eventID, true, ) delivered = receiptWithDeliveredVolumes(delivered, view) - delivery, renderErr := renderOverviewDelivery(overviewRenderContext{ + renderCtx := overviewRenderContext{ Root: root, MCPServiceVersion: mcpServiceVersion, LayoutMode: string(loaded.set.LayoutMode), RequestedScope: view.RequestedScope, EffectiveScope: view.EffectiveScope, AssetState: view.AssetState, @@ -105,20 +154,52 @@ func handleVolumeOverview( EntryCount: view.ObjectCount, SectionCount: countVolumeSections(view), EstimatedTokens: volumeScopeBytes(view) / 3, Receipt: delivered, Assessment: assessment, Sequence: sequence, - Session: refreshSession, - }, input, loaded.cfg.OverviewDelivery.ChunkTokens) + Session: refreshSession, LedgerEnabled: loaded.cfg.LedgerEnabled, + } + var entry *volumeDelivery + if guard != nil { + prepared, err := prepareOverview(renderCtx, loaded.cfg.OverviewDelivery.ChunkTokens) + if err != nil { + return errResult(errBadArgs, err.Error(), "") + } + renderCtx.Prepared = prepared + entry, err = newVolumeDelivery(owner, renderCtx, guard, semantic, governanceIdentity, loaded.cfg.OverviewDelivery.ChunkTokens) + if err != nil { + return errResult(errInternal, err.Error(), "") + } + renderCtx = entry.ctx + } + delivery, renderErr := renderOverviewDelivery(renderCtx, input, loaded.cfg.OverviewDelivery.ChunkTokens) if renderErr != nil { return errResult(errBadArgs, renderErr.Error(), "") } if snapshotFail := confirmCognitionSnapshot(root, loaded.set); snapshotFail != nil { return failResult(snapshotFail) } - if snapshotFail := confirmVolumeGovernanceSnapshot(root, loaded, governanceIdentity); snapshotFail != nil { - return failResult(snapshotFail) + if guard != nil { + if err := guard.validate(root, loaded); err != nil { + return overviewSnapshotFailure() + } + if err := refreshSession.deliveries.put(entry); err != nil { + return errResult(errBadArgs, err.Error(), "") + } + if err := rewriteVolumeCursor(&delivery, entry); err != nil { + refreshSession.deliveries.remove(entry) + return errResult(errInternal, err.Error(), "") + } + if deliveryCompleted(delivery, entry) { + refreshSession.deliveries.completed(entry) + } + } else { + work.GovernanceEvals++ + if snapshotFail := confirmVolumeGovernanceSnapshot(root, loaded, governanceIdentity); snapshotFail != nil { + return failResult(snapshotFail) + } } refreshSession.recordAttestedDelivery(delivered, delivery.Attestation, markDelivered) ledger.Append(root, loaded.cfg.LedgerEnabled, delivery.Facts.ledgerEvent(time.Since(start).Milliseconds())) - return textResult(delivery.Output) + recordOverviewProofEvents(root, loaded.cfg.LedgerEnabled, input, delivery, framed) + return overviewWorkResult(delivery, work) } func renderAbsentVolumeOverview( diff --git a/internal/volumegovernance/facts.go b/internal/volumegovernance/facts.go index dc26bf36..dc72c21e 100644 --- a/internal/volumegovernance/facts.go +++ b/internal/volumegovernance/facts.go @@ -393,6 +393,19 @@ func BoundListsForTransport(facts *Facts, limit int) *Facts { // governance authorities. The result is deterministic for the same formal // bytes, Baseline, configuration, Curation, and saved Database Evidence. func Assess(root string, cfg *config.Config, set *cognition.Set) (*Facts, error) { + return assess(root, cfg, set, nil) +} + +// AssessWithSnapshot returns the same governance verdict and the Code source +// snapshot that actually determined it. It does not rescan or relax a check. +// The caller may reuse the verdict only while all of its inputs remain equal. +func AssessWithSnapshot(root string, cfg *config.Config, set *cognition.Set) (*Facts, *managedstate.State, error) { + var snapshot *managedstate.State + facts, err := assess(root, cfg, set, &snapshot) + return facts, snapshot, err +} + +func assess(root string, cfg *config.Config, set *cognition.Set, snapshot **managedstate.State) (*Facts, error) { if cfg == nil || set == nil || set.LayoutMode != cognition.LayoutVolumesV1 { return nil, fmt.Errorf("volumes_governance_input_invalid") } @@ -426,7 +439,7 @@ func Assess(root string, cfg *config.Config, set *cognition.Set) (*Facts, error) facts.EnabledDomains = append(facts.EnabledDomains, cognition.ScopeCode) facts.Code = assetFacts(asset, true) facts.CodeEntryCount = asset.ObjectCount - assessCode(root, cfg, set, state, facts) + assessCode(root, cfg, set, state, facts, snapshot) } if asset := enabledAsset(set, cognition.ScopeDatabase); asset != nil { facts.EnabledDomains = append(facts.EnabledDomains, cognition.ScopeDatabase) @@ -534,7 +547,7 @@ func emptyDrift() Drift { Skipped: []string{}, CurationExcluded: []string{}} } -func assessCode(root string, cfg *config.Config, set *cognition.Set, baselineState *baseline.Baseline, facts *Facts) { +func assessCode(root string, cfg *config.Config, set *cognition.Set, baselineState *baseline.Baseline, facts *Facts, snapshot **managedstate.State) { asset := set.Volumes[cognition.ScopeCode] ownershipConflicts := map[string]cognition.OwnershipConflict{} ownershipOrphans := []string{} @@ -558,6 +571,9 @@ func assessCode(root string, cfg *config.Config, set *cognition.Set, baselineSta facts.Findings = append(facts.Findings, Finding{Code: "managed_scope_invalid", Domain: cognition.ScopeCode}) return } + if snapshot != nil { + *snapshot = managed + } facts.ManagedScope = ManagedScopeFacts{Aligned: !managed.ScopeChangeRequired, ScopeChangeRequired: managed.ScopeChangeRequired, PolicyIdentity: managed.DesiredPolicyIdentity, ActivePolicyIdentity: managed.ActivePolicyIdentity} diff --git a/scripts/blackbox/mcp_scenarios.py b/scripts/blackbox/mcp_scenarios.py index 7e21a68f..3ffcb0be 100644 --- a/scripts/blackbox/mcp_scenarios.py +++ b/scripts/blackbox/mcp_scenarios.py @@ -327,21 +327,44 @@ def group_a(): # A2: tampered ordinal inside the cursor parts = cur2.split(":") - bad_ord = ":".join([parts[0], parts[1], str(int(parts[2]) + 37), parts[3]]) + # V2 wraps the legacy payload with a session id and MAC. Mutating the + # payload must fail without repairing/re-signing its authentication. + v2 = parts[0] == "v2" + ordinal_offset, sha_offset = (4, 5) if v2 else (2, 3) + ordinal_parts = parts.copy() + ordinal_parts[ordinal_offset] = str(int(parts[ordinal_offset]) + 37) + bad_ord = ":".join(ordinal_parts) t, err = text_of(s.call("aoci_overview", {"cursor": bad_ord})) record(g, "A2.tampered-ordinal", "PASS" if is_rejection(t, err) else "FAIL", t[:150]) # A3: tampered previous-chunk sha - bad_sha = ":".join(parts[:3] + ["0" * 64]) + sha_parts = parts.copy() + sha_parts[sha_offset] = "0" * 64 + bad_sha = ":".join(sha_parts) t, err = text_of(s.call("aoci_overview", {"cursor": bad_sha})) record(g, "A3.tampered-prev-sha", "PASS" if is_rejection(t, err) else "FAIL", t[:150]) - # A4: cursor across server processes. Documented: with an unchanged Index and - # chunk_tokens, the same cursor is accepted across MCP process restarts. + # A4: v2 state is intentionally ephemeral and must reject after restart. + # Legacy v1 remains reconstructable with live governance validation. s2 = Session(REAL) t, err = text_of(s2.call("aoci_overview", {"cursor": cur2})) mx, bx = meta_and_body(t) - if bx is not None: + if v2: + rejected = bx is None and is_rejection(t, err) and "overview_session_unavailable_restart_required" in t + # Keep the old wire compatibility promise under the same safety scenario. + legacy_cursor = ":".join(parts[2:6]) + legacy_text, legacy_err = text_of(s2.call("aoci_overview", {"cursor": legacy_cursor})) + legacy_meta, legacy_body = meta_and_body(legacy_text) + original_text, original_err = text_of(s.call("aoci_overview", {"cursor": cur2})) + original_meta, original_body = meta_and_body(original_text) + compatible = (not legacy_err and not original_err and legacy_body is not None + and legacy_body == original_body + and legacy_meta.get("chunk_sha256") == original_meta.get("chunk_sha256")) + ok = rejected and compatible + record(g, "A4.cross-session-cursor", "PASS" if ok else "FAIL", + "v2 rejected safely after restart; v1 reconstructs identical chunk bytes" + if ok else f"v2_rejected={rejected} v1_compatible={compatible}: {t[:100]}") + elif bx is not None: ok = hashlib.sha256(bx.encode()).hexdigest() == mx.get("chunk_sha256") record(g, "A4.cross-session-cursor", "PASS" if ok else "FAIL", "deterministic re-derivation, chunk sha self-consistent" if ok else "served chunk failed its own sha") @@ -583,11 +606,8 @@ def group_d(bigfx=None): record(g, "D2.index-change-mid-chain", "PASS" if ok else "FAIL", t[:150]) sC.close() - # D3: baseline-only change mid-chain (identical entry text) — the formal - # Index bytes are unchanged, so per the documented cursor semantics the - # continuation must proceed (delivered bytes == current formal bytes). - # Baseline participates only in the session-scoped Level-4 governance - # binding, never in the chunk-chain stop list. + # D3: V2 binds Baseline bytes and must reject baseline-only drift. + # V1 retains its old byte-compatible continuation with live governance. sC = Session(fx) t, _ = text_of(sC.call("aoci_overview")) mm, _ = meta_and_body(t) @@ -598,9 +618,20 @@ def group_d(bigfx=None): st = land_write(fx, "f012.go", "Provides fixture constant unit 12") # unchanged text => index bytes stable t, err = text_of(sC.call("aoci_overview", {"cursor": cur})) mc, bc = meta_and_body(t) - if st == "applied" and not err and (mc.get("completed") or mc.get("chunk_index")): + if cur.startswith("v2:"): + stopped = bc is None and is_rejection(t, err) and "overview_snapshot_changed" in t + legacy_cursor = ":".join(cur.split(":")[2:6]) + legacy_text, legacy_err = text_of(sC.call("aoci_overview", {"cursor": legacy_cursor})) + legacy_meta, legacy_body = meta_and_body(legacy_text) + compatible = (not legacy_err and legacy_body is not None + and legacy_meta.get("index_sha256") == mm.get("index_sha256")) + ok = st == "applied" and stopped and compatible + record(g, "D3.baseline-only-mid-chain", "PASS" if ok else "FAIL", + "v2 rejects Baseline drift; v1 preserves byte-compatible continuation" + if ok else f"v2_stopped={stopped} v1_compatible={compatible}: {t[:100]}") + elif st == "applied" and not err and (mc.get("completed") or mc.get("chunk_index")): record(g, "D3.baseline-only-mid-chain", "PASS", - "continuation proceeds; index bytes unchanged, Baseline only binds the Level-4 governance plane") + "v1 proceeds with unchanged Index bytes and live governance") else: record(g, "D3.baseline-only-mid-chain", "FAIL", t[:150]) sC.close() diff --git a/spec/public/aoci-overview-delivery-v1.txt b/spec/public/aoci-overview-delivery-v1.txt index 44110e49..8890de22 100644 --- a/spec/public/aoci-overview-delivery-v1.txt +++ b/spec/public/aoci-overview-delivery-v1.txt @@ -78,16 +78,51 @@ Attestation answer ordinals, and object coverage all consume this exact shared ordered sequence. There is no display ordinal, line-number ordinal, Chunk-local ordinal, or implicit offset. -The opaque cursor binds the Index SHA-256, configured `chunk_tokens`, next Entry -ordinal, and previous Chunk SHA-256. Invalid, missing, reordered, or cross-chain -use fails closed; an exact replay of a genuine cursor idempotently re-serves -the identical Chunk bytes. A changed Index returns -`overview_snapshot_changed` and requires a restart at Chunk 1. A changed -configuration returns `overview_chunk_tokens_changed` and also requires a -restart. AOCI persists no delivery transaction or Session. The cursor is -deterministically re-derivable from the bound facts alone, so an unchanged -Index and `chunk_tokens` accept the same cursor across MCP process restarts; -the Volumes governance binding remains in-memory session state. +The reconstructable v1 cursor binds the Index SHA-256, configured +`chunk_tokens`, next Entry ordinal, and previous Chunk SHA-256. Its existing +live-governance path remains compatible across MCP process restarts. + +Eligible Volumes deliveries use an ephemeral v2 cursor. It additionally binds +an unpredictable delivery id, the originating MCP Host session, and an HMAC +under a private per-delivery secret. The server retains immutable canonical +body bytes, Entry sequence, Challenge, and chunk spans; it retains no mutable +chunk position. Exact replay idempotently returns the same chunk body. A +foreign Host, changed token, unavailable session, expiry, eviction, or process +restart fails closed and requires a fresh request at Chunk 1. No delivery state +is persisted. The chunk receipt remains `overview-chunk-receipt/v1`; only the +opaque continuation token is versioned. + +The initial eligible request runs the complete governance assessment once and +captures its actual managed-state fingerprints. Before publishing it, a strict +input guard confirms the effective configuration and raw base/local config, +Baseline and Curation bytes, the composite CognitionSet identity and every +formal asset, Safe Inventory membership and Git tracked/ignored status, safety +rules identity/blockers, and raw SHA-256 and size of every index/observe source. +Continuation repeats this guard before and after rendering. It does not trust +mtime, rerun policy evaluation over every excluded candidate, or reconstruct +body/sequence/Challenge/spans. Recovery and pending AtomicWrite intents still +fail closed. Even a same-size change with restored mtime invalidates the chain. +Commit-only HEAD changes are not governance drift, as in the existing binding; +Ledger and other safety-excluded runtime records are not source identities. + +Database-enabled deliveries retain the original live-governance path because +Evidence expiry and connector state require a separate reusable guard. A Code +snapshot with pending scope-policy alignment or unreadable source warnings also +retains the original path. The optimization never upgrades a dirty verdict. + +Each MCP server retains at most 32 deliveries and 64 MiB of conservatively +accounted retained payloads, including proof evidence. Proof replacement reserves +a conservative high-water byte cost before retaining it; growth evicts other +deliveries as needed, and an oversize proof fails explicitly without scoring. +A delivery expires after 15 idle minutes or 60 +minutes from creation, even with active replay. Insertion evicts the oldest +used delivery as necessary; oversize deliveries fail explicitly. Cleanup occurs +on store operations. A held request may finish from its immutable snapshot +when concurrent insertion evicts its store entry; future cursors fail cleanly. + +Successful Volumes responses expose optional MCP result `_meta.aoci_work` with +`governance_evals` and `body_builds`, counted at the actual full-work call sites. +These diagnostic counters do not participate in cognition identity or scoring. Full-body receipt and Host confirmation --------------------------------------- @@ -314,3 +349,22 @@ manual fallback or diagnostic only. It does not provide equivalent Chunk-chain integrity, governance or Baseline state, pending-transaction protection, or Attestation proof. v1 adds no file-read Attestation, command, tool, or second state machine. + +Attestation schema diagnostics +------------------------------ + +Every `model_cognition_attestation.challenge_answers` item requires exactly +`ordinal`, `object_identity`, `tag`, and `core_f`, with +`additionalProperties=false`. The MCP Schema already carries this structure; +the tool description also spells it out for Hosts that flatten nested items. +`object` and `object_ref` are not aliases. A schema rejection explicitly names +`object_identity` and occurs before the semantic scorer. Host-side conversion +of the published JSON Schema is outside the server and is not proven repaired +by the description fallback. + +Ledger operations separately record `overview_delivery_completed`, +`overview_host_confirmation_accepted`, `overview_attestation_schema_rejected`, +`overview_attestation_schema_accepted`, `overview_attestation_scoring_started`, +and `overview_attestation_scored` (result pass, partial, or fail). Schema errors +carry no cognition-level or semantic-score claim. Semantic thresholds and +strict proof fields are unchanged. diff --git a/testdata/golden/mcp_list_tools.json b/testdata/golden/mcp_list_tools.json index 076764f4..5e784606 100644 --- a/testdata/golden/mcp_list_tools.json +++ b/testdata/golden/mcp_list_tools.json @@ -94,7 +94,7 @@ "_meta": { "anthropic/maxResultSizeChars": 200000 }, - "description": "交付完整正式Whole-Index;Volumes v1仓库可选all、project、meta、code或database scope。普通调用在正文可装入overview_delivery.chunk_tokens时一次交付,否则立即返回使用overview-chunk-receipt/v1的第1块。看到continuation_required=true后必须原样提交next_cursor并自动继续到completed=true;不得询问用户、开始业务任务、给出阶段性结论,或用scope、aoci_search、aoci_get_entries补齐缺块。Host截断、缺块、重复、乱序、cursor失败或Index/配置变化时停止本次认知链。正式Entry ordinal是唯一规范序列中的1-based位置:Header、注释、空行、Section/Overview/Chunk Marker、Receipt与Metadata均不计数,Chunk Receipt与Challenge答案消费同一序列。完整链结束后只正式提交一次host_delivery_confirmation和既有model-cognition-attestation/v1;Attestation必须原样回绑最终Challenge发布的当前index_sha256、entry_sequence_sha256与entry_count,旧身份无效。同一响应只允许一次纯Schema修正,语义不匹配即fail/uncertain,禁止读取Memory、源码、Spec、aoci.txt、历史会话或调用Search、Get Entries、Scope旁路补答。Attestation为partial或fail时禁止无保留地声称完整系统认知,但不禁止诚实回答或source-bound调查与工程。ordinal、对象身份、Tag或核心F不匹配属于严格证明失败,本身不能证明模型没有理解系统。Host可以设置cognition_state_version=cognition-state/v2,接收把delivery_verified、model_cognition_usable、strict_attestation_verified、governance_aligned与current_system_cognition_reliable分开的增量只读投影;省略该字段时保持精确Legacy响应形状。上下文压缩刷新若传输完整、Host确认且治理对齐,则即使Attestation为partial或fail也消费该generation,Host继续原source-bound任务而不再次自动Overview。首次失败还会阻断未绑定源码的Root/Meta、Migration、全局布局和其他系统决策。system_mastery_percent只自评系统框架——架构、职责、强关系、稳定外部契约以及高熵安全和维护约束——不表示完整源码或运行实况知识。默认只向用户展示一句把真实索引覆盖率与框架掌握度分开的简洁结果。check_only=true只返回紧凑checkpoint,不进入分块链。正式认知Dirty或Stale时仍可交付但不可靠;aoci_maintain仍是治理路径,未决Recovery或不一致snapshot失败关闭。正文只包含开始Marker、原始aoci.txt字节和结束Marker,不生成目录清单。Legacy无参数与Volumes scope行为保持兼容。不会修改正式索引或Baseline,但可能追加本地审计记录。", + "description": "交付完整正式Whole-Index;Volumes v1仓库可选all、project、meta、code或database scope。普通调用在正文可装入overview_delivery.chunk_tokens时一次交付,否则立即返回使用overview-chunk-receipt/v1的第1块。看到continuation_required=true后必须原样提交next_cursor并自动继续到completed=true;不得询问用户、开始业务任务、给出阶段性结论,或用scope、aoci_search、aoci_get_entries补齐缺块。Host截断、缺块、重复、乱序、cursor失败或Index/配置变化时停止本次认知链。正式Entry ordinal是唯一规范序列中的1-based位置:Header、注释、空行、Section/Overview/Chunk Marker、Receipt与Metadata均不计数,Chunk Receipt与Challenge答案消费同一序列。完整链结束后只正式提交一次host_delivery_confirmation和既有model-cognition-attestation/v1;Attestation必须原样回绑最终Challenge发布的当前index_sha256、entry_sequence_sha256与entry_count,旧身份无效。同一响应只允许一次纯Schema修正,语义不匹配即fail/uncertain,禁止读取Memory、源码、Spec、aoci.txt、历史会话或调用Search、Get Entries、Scope旁路补答。Attestation为partial或fail时禁止无保留地声称完整系统认知,但不禁止诚实回答或source-bound调查与工程。ordinal、对象身份、Tag或核心F不匹配属于严格证明失败,本身不能证明模型没有理解系统。Host可以设置cognition_state_version=cognition-state/v2,接收把delivery_verified、model_cognition_usable、strict_attestation_verified、governance_aligned与current_system_cognition_reliable分开的增量只读投影;省略该字段时保持精确Legacy响应形状。上下文压缩刷新若传输完整、Host确认且治理对齐,则即使Attestation为partial或fail也消费该generation,Host继续原source-bound任务而不再次自动Overview。首次失败还会阻断未绑定源码的Root/Meta、Migration、全局布局和其他系统决策。system_mastery_percent只自评系统框架——架构、职责、强关系、稳定外部契约以及高熵安全和维护约束——不表示完整源码或运行实况知识。默认只向用户展示一句把真实索引覆盖率与框架掌握度分开的简洁结果。check_only=true只返回紧凑checkpoint,不进入分块链。正式认知Dirty或Stale时仍可交付但不可靠;aoci_maintain仍是治理路径,未决Recovery或不一致snapshot失败关闭。正文只包含开始Marker、原始aoci.txt字节和结束Marker,不生成目录清单。Legacy无参数与Volumes scope行为保持兼容。不会修改正式索引或Baseline,但可能追加本地审计记录。\nmodel_cognition_attestation.challenge_answers 的每项都是对象,必须且只能包含 ordinal、object_identity、tag、core_f;additionalProperties=false。object 和 object_ref 无效。Schema 拒绝发生在语义评分前。", "inputSchema": { "additionalProperties": false, "properties": { diff --git a/textassets/en-US/contracts/mcp/tools/overview-description.txt b/textassets/en-US/contracts/mcp/tools/overview-description.txt index b1d8e589..59b6bea3 100644 --- a/textassets/en-US/contracts/mcp/tools/overview-description.txt +++ b/textassets/en-US/contracts/mcp/tools/overview-description.txt @@ -1 +1,2 @@ Deliver the complete formal Whole-Index, optionally scoped to all, project, meta, code, or database in a Volumes v1 repository. An ordinary call returns the exact formal body inline when it fits overview_delivery.chunk_tokens, otherwise it immediately returns Chunk 1 using overview-chunk-receipt/v1. When continuation_required=true, submit the exact next_cursor automatically until completed=true; do not ask the user, start the business task, state a partial conclusion, or replace missing Chunks with scope, aoci_search, or aoci_get_entries. Stop the cognition chain on Host truncation, a missing, duplicate, or reordered Chunk, cursor failure, or Index/configuration change. The formal Entry ordinal is the 1-based position in one canonical sequence: Header, comments, blank lines, Section/Overview/Chunk Marker, Receipt, and Metadata are excluded, and Chunk Receipts and Challenge answers consume that same sequence. After the complete chain, submit host_delivery_confirmation and the existing model-cognition-attestation/v1 once; the Attestation must echo the final Challenge's exact current index_sha256, entry_sequence_sha256, and entry_count, and a prior identity is invalid. One same-response schema-only correction is allowed, but semantic mismatch means fail/uncertain with no Memory, source, Spec, aoci.txt, Search, Get Entries, Scope, or historical-session repair. A failed or partial Attestation disables an unqualified complete-system claim; it does not prohibit honest answers or source-bound investigation and engineering. An ordinal, object identity, Tag, or core F mismatch is strict proof failure, not by itself proof that the model did not understand the system. A Host may set cognition_state_version=cognition-state/v2 to receive an additive read-only projection that separates delivery_verified, model_cognition_usable, strict_attestation_verified, governance_aligned, and current_system_cognition_reliable; omitting it preserves the exact legacy response shape. A complete, host-confirmed, governance-aligned context-compaction refresh consumes its generation even when Attestation is partial or failed, so the Host continues the existing source-bound task without another automatic Overview. Initial failure additionally blocks unbound Root/Meta, Migration, layout-wide, and other system decisions. system_mastery_percent self-assesses the system framework—architecture, responsibilities, strong relationships, stable external contracts, and high-entropy safety and maintenance constraints—not complete source or runtime knowledge. Normally expose only one concise user sentence separating actual Index coverage from framework mastery. check_only=true returns only compact checkpoint facts and never starts a Chunk chain. Dirty or Stale cognition is delivered but unreliable; aoci_maintain remains the governance path, while pending Recovery or an incoherent snapshot fails closed. The body is start marker, exact aoci.txt bytes, and end marker with no generated directory inventory. Legacy no-argument and Volumes scope behavior remain compatible. This does not modify formal cognition or Baseline, but may append a local audit record. +Every model_cognition_attestation.challenge_answers item is an object requiring exactly ordinal, object_identity, tag, core_f; additionalProperties=false. object and object_ref are invalid. Schema rejection occurs before semantic scoring. diff --git a/textassets/en-US/contracts/ui/messages.json b/textassets/en-US/contracts/ui/messages.json index 98ec519c..fc672fac 100644 --- a/textassets/en-US/contracts/ui/messages.json +++ b/textassets/en-US/contracts/ui/messages.json @@ -1765,5 +1765,6 @@ "ui.none_running": "No AOCI panel is running for this repository.", "ui.error.detach_failed": "could not start the panel in the background: %s", "ui.error.registry": "cannot locate the panel registry (user cache directory): %s", - "ui.error.legacy_page": "an unauthenticated legacy panel is still running; run aoci ui --stop for this repository before starting a new panel" + "ui.error.legacy_page": "an unauthenticated legacy panel is still running; run aoci ui --stop for this repository before starting a new panel", + "overview.attestation.schema_expected": "Attestation schema rejected before semantic scoring. Every challenge_answers item requires exactly ordinal, object_identity, tag, core_f with additionalProperties=false. Use object_identity; object and object_ref are invalid." } diff --git a/textassets/zh-CN/contracts/mcp/tools/overview-description.txt b/textassets/zh-CN/contracts/mcp/tools/overview-description.txt index 14828bcd..a7046b91 100644 --- a/textassets/zh-CN/contracts/mcp/tools/overview-description.txt +++ b/textassets/zh-CN/contracts/mcp/tools/overview-description.txt @@ -1 +1,2 @@ 交付完整正式Whole-Index;Volumes v1仓库可选all、project、meta、code或database scope。普通调用在正文可装入overview_delivery.chunk_tokens时一次交付,否则立即返回使用overview-chunk-receipt/v1的第1块。看到continuation_required=true后必须原样提交next_cursor并自动继续到completed=true;不得询问用户、开始业务任务、给出阶段性结论,或用scope、aoci_search、aoci_get_entries补齐缺块。Host截断、缺块、重复、乱序、cursor失败或Index/配置变化时停止本次认知链。正式Entry ordinal是唯一规范序列中的1-based位置:Header、注释、空行、Section/Overview/Chunk Marker、Receipt与Metadata均不计数,Chunk Receipt与Challenge答案消费同一序列。完整链结束后只正式提交一次host_delivery_confirmation和既有model-cognition-attestation/v1;Attestation必须原样回绑最终Challenge发布的当前index_sha256、entry_sequence_sha256与entry_count,旧身份无效。同一响应只允许一次纯Schema修正,语义不匹配即fail/uncertain,禁止读取Memory、源码、Spec、aoci.txt、历史会话或调用Search、Get Entries、Scope旁路补答。Attestation为partial或fail时禁止无保留地声称完整系统认知,但不禁止诚实回答或source-bound调查与工程。ordinal、对象身份、Tag或核心F不匹配属于严格证明失败,本身不能证明模型没有理解系统。Host可以设置cognition_state_version=cognition-state/v2,接收把delivery_verified、model_cognition_usable、strict_attestation_verified、governance_aligned与current_system_cognition_reliable分开的增量只读投影;省略该字段时保持精确Legacy响应形状。上下文压缩刷新若传输完整、Host确认且治理对齐,则即使Attestation为partial或fail也消费该generation,Host继续原source-bound任务而不再次自动Overview。首次失败还会阻断未绑定源码的Root/Meta、Migration、全局布局和其他系统决策。system_mastery_percent只自评系统框架——架构、职责、强关系、稳定外部契约以及高熵安全和维护约束——不表示完整源码或运行实况知识。默认只向用户展示一句把真实索引覆盖率与框架掌握度分开的简洁结果。check_only=true只返回紧凑checkpoint,不进入分块链。正式认知Dirty或Stale时仍可交付但不可靠;aoci_maintain仍是治理路径,未决Recovery或不一致snapshot失败关闭。正文只包含开始Marker、原始aoci.txt字节和结束Marker,不生成目录清单。Legacy无参数与Volumes scope行为保持兼容。不会修改正式索引或Baseline,但可能追加本地审计记录。 +model_cognition_attestation.challenge_answers 的每项都是对象,必须且只能包含 ordinal、object_identity、tag、core_f;additionalProperties=false。object 和 object_ref 无效。Schema 拒绝发生在语义评分前。 diff --git a/textassets/zh-CN/contracts/ui/messages.json b/textassets/zh-CN/contracts/ui/messages.json index 791cd185..c780142c 100644 --- a/textassets/zh-CN/contracts/ui/messages.json +++ b/textassets/zh-CN/contracts/ui/messages.json @@ -1765,5 +1765,6 @@ "ui.none_running": "本仓库没有正在运行的 AOCI 面板。", "ui.error.detach_failed": "无法在后台启动面板:%s", "ui.error.registry": "无法定位面板登记目录(用户缓存目录):%s", - "ui.error.legacy_page": "旧版未认证面板仍在运行;请先对本仓库运行 aoci ui --stop,再启动新面板" + "ui.error.legacy_page": "旧版未认证面板仍在运行;请先对本仓库运行 aoci ui --stop,再启动新面板", + "overview.attestation.schema_expected": "Attestation Schema 在语义评分前拒绝了请求。challenge_answers 的每项必须且只能包含 ordinal、object_identity、tag、core_f,additionalProperties=false。请使用 object_identity;object 和 object_ref 无效。" }