Ars Umbris scope evidence
Read-only local object inspection for the comparison. No checkout materialization, acquisition, dependency installation or target execution. Full pins resolve entry VERSION.md abbreviations; branch HEADs were not used as evidence pins. Extract line numbers refer to committed blobs. SHA-256 values cover whole blob bytes, not rendered extracts.
Available revisions
| Repository | Full commit | Local release tag equals pin |
|---|---|---|
arsumbris |
a9f6cceedb191b8504443222d55d3106f9f36ba9 |
yes |
au-engine |
46f12389d4d4833fed51640a48cf938516433106 |
yes |
au-engine-sdk |
ffeca7a745af6f8069d5d2151fc96bae964825dd |
yes |
au-mcp |
3ddc3b62ed0b339a89a57f11026676df18d3b2eb |
yes |
au-mcp-sdk |
91209bb00cebde36e6454207a89e257f20573577 |
yes |
au-mcp-core |
25e0625e553c237671bba9a3a8c173a889dddf02 |
yes |
au-mcp-adapter-cc |
333b2117f338cd4815eeeb9ab7b3932b29fa7d62 |
yes |
au-mcp-adapter-codex |
3b0e3a3cf67b3143819c61c590efc85d36c23bc5 |
yes |
au-defaults |
0879c5ef432b468b9fa0e4a3e52d2fb58d2ec504 |
yes |
au-type-system |
066fd06b224bc46358273ebfbc82b6b8863e9885 |
yes |
au-host |
0e85fb1731fdef2d6196618aff5a2a567e621866 |
yes |
au-type-codegen |
e3c63da4afc77c2979ea06763c8c1045bcd93644 |
yes |
Discriminating extracts
arsumbris: VERSION.md, lines 1–33
Whole-blob SHA-256: 24dfb38dd523ba19b5f271e4ef61c141cc24361c0bd583346bc02c7ace6adaca. Revision: a9f6cceedb191b8504443222d55d3106f9f36ba9.
1: <!-- generated by scripts/release.py at release time -->
2: <!-- do not edit by hand -->
3:
4: # arsumbris 0.0.1-alpha
5:
6: The parts this release ships. Each `commit` is that part's public repository commit tagged `0.0.1-alpha`.
7:
8: | part | commit | internal version |
9: | --- | --- | --- |
10: | arsumbris | — | — |
11: | au-engine | 46f1238 | wire schema 29 |
12: | au-engine-sdk | ffeca7a | — |
13: | au-type-system | 066fd06 | — |
14: | au-host | 0e85fb1 | mount contract 7 |
15: | au-mcp | 3ddc3b6 | mcp wire 0 |
16: | au-mcp-sdk | 91209bb | — |
17: | au-mcp-core | 25e0625 | — |
18: | au-mcp-adapter-cc | 333b211 | — |
19: | au-mcp-adapter-codex | 3b0e3a3 | — |
20: | au-type-codegen | e3c63da | — |
21: | au-defaults | 0879c5e | — |
22: | au-base-types | 0a0c977 | — |
23: | au-weave | 2432073 | — |
24: | au-agent-guides | ddc3333 | — |
25: | au-rules | 4be3274 | — |
26: | au-writing-style | 163b559 | — |
27: | au-skills | d32336d | — |
28: | au-govern | 8bf899b | — |
29: | au-competency | 6f23713 | — |
30: | au-ingest | 5b3c22f | — |
31: | au-tree-research | 65faf72 | — |
au-defaults: mcp-bundle/.arsumbris/repo.yaml, lines 1–11
Whole-blob SHA-256: 7ffc741c782650c72b947b370f33eebed91d6c57e6548a5acec59d80e8a7abc1. Revision: 0879c5ef432b468b9fa0e4a3e52d2fb58d2ec504.
1: # The agent/mcp framework bundle
2: # A pure AGGREGATOR node (au.engine.repo).
3: type: au.engine.repo::au-engine
4: name: mcp-bundle
5: description: aggregator for the agent/mcp framework type-def owners
6: deps:
7: - name: au-mcp
8: - name: au-mcp-sdk
9: - name: au-mcp-core
10: - name: au-mcp-adapter-cc
11: - name: au-mcp-adapter-codex
au-defaults: studio/profiles/Claude.yaml, lines 1–2
Whole-blob SHA-256: 82722d3c85581ee9de20123b743705f4869aa618face5b2e6a75c5412e63849a. Revision: 0879c5ef432b468b9fa0e4a3e52d2fb58d2ec504.
1: type: agent-profile::au-mcp-sdk
2: adapter: "[[mcp.adapter.cc::au-mcp-adapter-cc]]"
au-mcp: package.json, lines 20–27
Whole-blob SHA-256: a4e50ea04bbbd7db4ace57e0eef7c8d39fc2964bd84602eaf792a0821efe7fab. Revision: 3ddc3b62ed0b339a89a57f11026676df18d3b2eb.
20: "@arsumbris/type-codegen": "link:../au-type-codegen",
21: "@arsumbris/au-mcp-sdk": "link:../au-mcp-sdk"
22: },
23: "devDependencies": {
24: "@types/node": "^25.9.1",
25: "typescript": "^6.0.3",
26: "vitest": "^4.1.7"
27: }
au-mcp: src/daemon/discovery.ts, lines 15–23
Whole-blob SHA-256: 29ea7497ebdd733df7250345aed63cf188c7efef1b97539aa26a88699f7a39d1. Revision: 3ddc3b62ed0b339a89a57f11026676df18d3b2eb.
15: import * as path from 'node:path'
16: import { pathToFileURL } from 'node:url'
17:
18: import { buildIR, emitJsonSchema } from '@arsumbris/type-codegen'
19: import type { WireTypeDef } from '@arsumbris/type-codegen'
20:
21: import type { Plugin, PluginContext, PluginManifest, PluginModule, PluginShape, PluginTier } from '@arsumbris/au-mcp-sdk'
22: import { PLUGIN_CONTRACT_VERSION } from '@arsumbris/au-mcp-sdk'
23:
au-mcp: src/daemon/discovery.ts, lines 318–339
Whole-blob SHA-256: 29ea7497ebdd733df7250345aed63cf188c7efef1b97539aa26a88699f7a39d1. Revision: 3ddc3b62ed0b339a89a57f11026676df18d3b2eb.
318: if (!def.source?.file) continue
319: const derived = manifestFrom(def.name, metaRecord(block))
320: if (!derived) continue
321: // Enforce the kernel<->plugin handshake: exact equality against the kernel-expected
322: // PLUGIN_CONTRACT_VERSION (loud fail-closed, no override — decision 2609091649). A skewed
323: // plugin is REFUSED, never loaded at the wrong contract. Routed through the existing
324: // critical/non-critical ethos: a `critical` plugin fails the daemon closed (refuse to
325: // serve, like a critical load failure); a non-critical one is skipped with a loud warning.
326: if (derived.manifest.contractVersion !== PLUGIN_CONTRACT_VERSION) {
327: const msg = `plugin contract v${derived.manifest.contractVersion} != kernel-expected v${PLUGIN_CONTRACT_VERSION}`
328: if (derived.manifest.critical) failedCritical.push({ id: derived.manifest.id, error: msg })
329: else process.stderr.write(`au-mcp: skipping ${derived.manifest.id}: ${msg}\n`)
330: continue
331: }
332: // Provenance: a loadable tool is CONTRIBUTED by the package that owns its def (the
333: // subtype's owner repo).
334: derived.manifest.provenance = def.repo ?? derived.manifest.provenance
335: // Access: the SAME `tool-access-meta` level the tool declares for its own scoped broker
336: // (below) also rides its manifest, so a mediator classifies it at decide. `requestedBroker`
337: // reads the def's meta. TOOLS only — a hook declares no tool-access-meta and gets a
338: // read-only broker from the daemon.
339: if (derived.manifest.kind === 'tool') derived.manifest.access = requestedBroker(def)
au-mcp: src/daemon/daemon.ts, lines 754–775
Whole-blob SHA-256: 08e0d51517af8a4b3f4047cde65608ffd1ef180956f22b52d50ace2faf2b0384. Revision: 3ddc3b62ed0b339a89a57f11026676df18d3b2eb.
754: case 'mediate': {
755: const session = requireSession(request.session)
756: // The mediate phase: mediators decide in priority order, first non-allow wins.
757: // A mediator may emit() events (e.g. redirect's tool_denied), which the daemon
758: // appends to the log + fans to observers — the kernel sequences decide -> record,
759: // so deny and trace cannot desync. The context (the emit/approval/broker/accessOf
760: // handles) is shared with the post-tool `review` turn via `mediationContextFor`.
761: const ctx = mediationContextFor(session)
762: // CONTINUITY Tier-2 (required capability lost): fail the session closed on WRITES. A
763: // state-touching action (a mutator) is denied with the recovery notice; reads + non-write
764: // escapes pass. The runtime sibling of the load-time `poisoned` refusal, scoped to this
765: // session + to mutators.
766: if (continuityWriteBlocked(session) && ctx.accessOf(request.action) === 'read-write') {
767: return { kind: 'decision', id: request.id, decision: { kind: 'deny', reason: continuityBlockNotice(session) } }
768: }
769: // ALL mediators are now DISCOVERED plugins from au-mcp-core (plan 2608261532, Phase 5):
770: // the redirect (mcp.nativeToolRedirect @100), read-before-write (mcp.read-guard @40), and
771: // tool-precondition (mcp.tool-precondition @30). They ride `registry.mediators()`,
772: // priority-sorted in `runMediators`, and read per-session state via `MediationContext`
773: // (`nativeToolAllowlist`, `readHash`/`hasRead`). The kernel wires no floor literal.
774: const decision = await runMediators(registry.mediators(), request.action, ctx)
775: return { kind: 'decision', id: request.id, decision }
au-mcp: src/daemon/daemon.ts, lines 885–909
Whole-blob SHA-256: 08e0d51517af8a4b3f4047cde65608ffd1ef180956f22b52d50ace2faf2b0384. Revision: 3ddc3b62ed0b339a89a57f11026676df18d3b2eb.
885: // Gate-side validation: refuse a malformed input against mcp.tool.<tool> before
886: // acting. Scope to the tool's OWNER repo (its manifest `provenance`): every tool's def
887: // lives in the package that shipped it (au-mcp owns none).
888: const invalid = await checkToolInput(broker, request.tool, request.input, plugin.manifest.provenance)
889: if (invalid) return { kind: 'invoked', id: request.id, result: invalid, isError: true }
890: // STAMP ATTACH (the stamper shape): on a governed write, sanitize the input then run the
891: // registered stampers, injecting their stamp un-forgeably (overwriting any agent-supplied
892: // one), AFTER gate-side validation so the stamp is daemon-internal, not agent schema. The
893: // strip is unconditional for a stampable write (a session-less one gets no stamp but is
894: // still sanitized); a non-write / non-stampable tool is untouched (returns undefined).
895: let effInput = request.input
896: const injected = await stampInputForWrite(request.tool, request.input, registry.stampers(), {
897: session: invSessionId,
898: launch: invInfo ? launchOf(invInfo, invSession?.nativeToolAllowlist) : undefined,
899: now: () => new Date().toISOString(),
900: hookConfigs: invSession?.hookConfigs,
901: })
902: if (injected !== undefined) effInput = injected
903: const result = await plugin.invoke(effInput, {
904: session: invSessionId,
905: launch: invInfo ? launchOf(invInfo, invSession?.nativeToolAllowlist) : undefined,
906: // A read-only trace handle when the call carries a session, so a status/reporting
907: // callable computes its own payload instead of leaning on a mediator inject.
908: consultTrace: invSession ? makeConsultTrace(invSession) : undefined,
909: })
au-mcp: src/daemon/broker.ts, lines 238–271
Whole-blob SHA-256: 06c4d0ee50d095cc86e8451e5a4e712ef85bf64da99e47e14706cbdcaa9d6d60. Revision: 3ddc3b62ed0b339a89a57f11026676df18d3b2eb.
238: function runMutation(client: DaemonClient, verb: string, args: Record<string, unknown>): Promise<TypedMutate> {
239: const path = String(args.path ?? '')
240: const expectedHash = typeof args.expected_hash === 'string' ? args.expected_hash : undefined
241: // The daemon-injected stamps LIST (the stamper shape), when this write carries any. The mcp-sdk
242: // Stamps a stamper produced ride here as engine-sdk Stamps (the mirror-pin above proves the element
243: // shapes match); all fold into the write's ONE commit (engine schema 24's `stamps` rider).
244: const stamps = (args.stamps ?? undefined) as Stamp[] | undefined
245: // The daemon-injected `ensure_mixins` rider (engine schema 25), the type-claim sibling of `stamps`.
246: // Carried on the same three verbs the SDK accepts it on (write_file / edit_file / rename); mapped
247: // to the engine-sdk camelCase options. The optional strict flag is omitted unless explicit, so the
248: // engine default (strict) stands.
249: const ensureMixins = Array.isArray(args.ensure_mixins) && args.ensure_mixins.length > 0
250: ? (args.ensure_mixins as string[])
251: : undefined
252: const mixinOpts =
253: ensureMixins !== undefined
254: ? {
255: ensureMixins,
256: ...(typeof args.ensure_mixins_strict === 'boolean' ? { ensureMixinsStrict: args.ensure_mixins_strict } : {}),
257: }
258: : {}
259: // The daemon-injected `attribution` rider (engine schema 26), the commit-trailer sibling of `stamps`.
260: // Carried on write_file / edit_file / delete_file (the verbs the engine attribution rider accepts);
261: // mapped to the engine-sdk camelCase option. Absent/empty -> no option (the engine writes no trailer).
262: const attribution =
263: Array.isArray(args.attribution) && args.attribution.length > 0 ? (args.attribution as AttributionEntry[]) : undefined
264: switch (verb) {
265: case 'write_file':
266: return client.writeFile(path, String(args.content ?? ''), {
267: ...(expectedHash !== undefined ? { expectedHash } : {}),
268: ...(stamps ? { stamps } : {}),
269: ...mixinOpts,
270: ...(attribution ? { attribution } : {}),
271: })
au-mcp-core: src/read-guard.ts, lines 77–97
Whole-blob SHA-256: 8592c14a84b8f48010ce0b7d25c8e6b8b57bebdead28fb69cb717ee48e8ff7bf. Revision: 25e0625e553c237671bba9a3a8c173a889dddf02.
77: return async (action: PendingAction, ctx: MediationContext): Promise<Decision> => {
78: const gatePrefix = ctx.launch.gatePrefix
79: if (!gatePrefix || !action.tool.startsWith(gatePrefix)) return { kind: 'allow' }
80: const base = action.tool.slice(gatePrefix.length)
81: if (!OVERWRITE_TOOLS.has(base)) return { kind: 'allow' } // reads, edit_file (self-guards), etc.
82: const path = filePathOf(action.input)
83: if (!path) return { kind: 'allow' } // malformed; gate-side validation handles it
84: // An explicit expected_hash means the caller is doing its own CAS — let it through.
85: if (action.input && typeof action.input === 'object' && 'expected_hash' in action.input) return { kind: 'allow' }
86: // A CREATE (target does not exist) has nothing to clobber, so it needs no prior read.
87: if (!exists(path)) return { kind: 'allow' }
88: const seen = ctx.readHash(path)
89: if (seen === undefined) {
90: return deny(ctx, action, `read ${path} through the gate before overwriting it (read-before-write)`)
91: }
92: const now = await currentContentHash(ctx.broker, path)
93: if (now !== null && now !== seen) {
94: return deny(ctx, action, `${path} changed on disk since you read it — re-read it before overwriting (read-before-write)`)
95: }
96: return { kind: 'allow' }
97: }
au-mcp-core: src/file-tools.ts, lines 243–261
Whole-blob SHA-256: e007df645a757ac59a3dcb2ecadbe20ff036f5ac9888ac1941acf30d4c1204d8. Revision: 25e0625e553c237671bba9a3a8c173a889dddf02.
243: const content = optString(input, 'content')
244: if (!file_path) return fail("'file_path' is required")
245: if (content === undefined) return fail("'content' is required")
246: if (!isAbsolute(file_path)) return fail(`file_path must be absolute, got: ${file_path}`)
247: const guarded = guardWritePath(workspace, file_path)
248: if (guarded) return fail(guarded)
249: if (!broker || !broker.available()) return noEngine('write')
250: // expected_hash: explicit from the caller, else the read-guard mediator injects it
251: // (read-before-write). Absent -> the engine overwrites regardless (a new file).
252: const expected_hash = optString(input, 'expected_hash')
253: const args: Record<string, unknown> = { path: file_path, content }
254: if (expected_hash !== undefined) args.expected_hash = expected_hash
255: const stamps = optStamps(input)
256: if (stamps) args.stamps = stamps
257: forwardMixins(input, args)
258: const attribution = optAttribution(input)
259: if (attribution) args.attribution = attribution
260: try {
261: return mutationResult(workspace, await broker.mutate('write_file', args), file_path, 'wrote')
au-mcp-core: type/mcp.hook.read-guard.type.yaml, lines 20–27
Whole-blob SHA-256: 5d03ffc073d6a3719332213c2495b23cc4b057cf9c173460e8b45a236be27e8c. Revision: 25e0625e553c237671bba9a3a8c173a889dddf02.
20: extends: mcp.hook::au-mcp-sdk
21: meta:
22: - type: plugin-runtime-meta::au-mcp-sdk
23: entry: ./src/read-guard.ts
24: contractVersion: 0
25: shapes: [mediator]
26: tier: floor
27: critical: true
au-mcp-core: type/mcp.tool.write_file.type.yaml, lines 1–14
Whole-blob SHA-256: 628bc974141076db8bc53388ce2af2d83d2ec123155780ecc657b52f4270a68d. Revision: 25e0625e553c237671bba9a3a8c173a889dddf02.
1: extends: mcp.tool::au-mcp-sdk
2: fields:
3: #: absolute path to write; parent dirs are created.
4: file_path: String
5: #: the full file contents (overwrites any existing file).
6: content: String
7: meta:
8: - type: plugin-runtime-meta::au-mcp-sdk
9: entry: ./src/tool-write-file.ts
10: contractVersion: 0
11: - type: tool-access-meta::au-mcp-sdk
12: broker: read-write
13: - type: tool-presentation-meta::au-mcp-sdk
14: description: "Write a file, overwriting if it exists. Absolute path; parent dirs created. Prefer edit_file for partial changes. Writes require an editable member (a consumed dep/discover or read-only cache snapshot rejects)."
au-mcp-adapter-cc: hooks/pre-tool-use.ts, lines 8–24
Whole-blob SHA-256: 3f865c4850536841f6f809448508743bf6ff0d72a9f0842497d4c62747133529. Revision: 333b2117f338cd4815eeeb9ab7b3932b29fa7d62.
8: const payload = await readPayload(process.stdin)
9: const tool = String(payload.tool_name ?? '')
10: const input = payload.tool_input ?? {}
11:
12: const decision = await mediateAction(payload, { tool, input })
13:
14: if (decision.kind === 'deny') {
15: process.stdout.write(
16: JSON.stringify({
17: hookSpecificOutput: {
18: hookEventName: 'PreToolUse',
19: permissionDecision: 'deny',
20: permissionDecisionReason: decision.reason,
21: },
22: }),
23: )
24: process.exit(0)
au-mcp-adapter-cc: src/bridge.ts, lines 120–151
Whole-blob SHA-256: 5b097ee946e3b9a5a7bf90a8ae7b5a86684448b287a963c58fae01f2d4eba315. Revision: 333b2117f338cd4815eeeb9ab7b3932b29fa7d62.
120: /**
121: * Open a daemon connection for the payload's workspace, ensure the session
122: * exists (idempotent session-open declaring CC's surface), run `fn`, then close.
123: * Returns `fallback` if the daemon is unreachable or anything throws — capture
124: * must never break the hook.
125: */
126: async function withSession<T>(
127: payload: Record<string, unknown> | null,
128: fn: (client: DaemonClient, session: string) => Promise<T>,
129: fallback: T,
130: ): Promise<T> {
131: const { workspace, session } = resolveContext(payload)
132: const transport = await connectSocket(socketPath(workspace)).catch(() => null)
133: if (!transport) return fallback
134: const client = createDaemonClient(transport)
135: try {
136: await client.sessionOpen(
137: ccAdapterInfo(
138: session,
139: workspace,
140: resolveSessionHandle(),
141: resolveResume(payload),
142: resolveProfile(),
143: ),
144: )
145: return await fn(client, session)
146: } catch {
147: return fallback
148: } finally {
149: client.dispose()
150: transport.close()
151: }
au-mcp-adapter-cc: src/bridge.ts, lines 217–223
Whole-blob SHA-256: 5b097ee946e3b9a5a7bf90a8ae7b5a86684448b287a963c58fae01f2d4eba315. Revision: 333b2117f338cd4815eeeb9ab7b3932b29fa7d62.
217: /** Ask the daemon to decide a pending action. Allows by default if unreachable. */
218: export async function mediateAction(
219: payload: Record<string, unknown> | null,
220: action: PendingAction,
221: ): Promise<Decision> {
222: return withSession(payload, (client, session) => client.mediate(session, action), { kind: 'allow' })
223: }
au-mcp-adapter-cc: src/mcp-server.ts, lines 177–186
Whole-blob SHA-256: 060deba67ad41920becdc93a7d2d33e750699853e121a1347fdbcd9a2ed011ad. Revision: 333b2117f338cd4815eeeb9ab7b3932b29fa7d62.
177: if (!client) {
178: client = await reconnectDaemon(workspace)
179: if (!client) return fail('au-mcp daemon not reachable for this workspace — is it running?')
180: }
181:
182: try {
183: const { result, isError } = await client.invoke(handle, `mcp.${name}`, args)
184: return ok(result, isError)
185: } catch (e) {
186: // A tool/engine error rides back as a normal response (isError), not a throw. A THROW
au-engine-sdk: src/client.ts, lines 667–681
Whole-blob SHA-256: b4a4c725c04bf8cca5b5e503074105111d483059f8c90d26644c1f78cb7525a1. Revision: ffeca7a745af6f8069d5d2151fc96bae964825dd.
667: writeFile(path: string, content: string, options: WriteFileOptions = {}): Promise<TypedMutate> {
668: return this.issueMutate({
669: mutate: 'write_file',
670: path,
671: content,
672: ...(options.expectedHash !== undefined ? { expected_hash: options.expectedHash } : {}),
673: ...toWireStamps(options.stamps),
674: ...toWireEnsureMixins(options.ensureMixins, options.ensureMixinsStrict),
675: ...toWireAttribution(options.attribution),
676: })
677: }
678:
679: /**
680: * Issue a `mutate: edit_file` — exact string replacement. `oldString` must
681: * match exactly; by default it must be unique (its own read-before-write
au-engine-sdk: src/wire.ts, lines 10–12
Whole-blob SHA-256: bd1fda901136444eb14ae63c7cf61775e1fb60abea41069c5f03add2cde36886. Revision: ffeca7a745af6f8069d5d2151fc96bae964825dd.
10: * additive evolution (new fields, new frame types) does not.
11: */
12: export const WIRE_SCHEMA_VERSION = 29
au-mcp-sdk: src/plugin.ts, lines 30–34
Whole-blob SHA-256: 9c4bc12654f7b7d7dcf93d537e25af5c5573104b8ece90cd28ff785a8b341407. Revision: 91209bb00cebde36e6454207a89e257f20573577.
30: * Distinct from the wire `MCP_CONTRACT_VERSION` (the client<->daemon protocol version):
31: * the two contracts version INDEPENDENTLY, though both are `0` today. See
32: * [[decision - 2609091649 - the mcp wire and plugin contracts enforce exact-equality, loud fail-closed with no override::au-harness]].
33: */
34: export const PLUGIN_CONTRACT_VERSION = 0
au-mcp-sdk: src/wire.ts, lines 10–12
Whole-blob SHA-256: b2852a48ed80a4752fbb6119abe96872c29a0bf64a7a8a425289f2e8b83a6c2e. Revision: 91209bb00cebde36e6454207a89e257f20573577.
10:
11: /** The contract version; handshake is exact integer equality. */
12: export const MCP_CONTRACT_VERSION = 0
au-engine: crates/au-engine/src/serve.rs, lines 130–132
Whole-blob SHA-256: dcd7f1ac12dce9066078741081c8cc8cf01faa6a0296dd4bbdb929db4f454ea0. Revision: 46f12389d4d4833fed51640a48cf938516433106.
130: /// daemon's reported `schema_version` against the version this binary compiles,
131: /// and flag a stale process speaking an old wire.
132: pub const SCHEMA_VERSION: u32 = 29;
au-engine: crates/au-engine/src/serve.rs, lines 3944–3959
Whole-blob SHA-256: dcd7f1ac12dce9066078741081c8cc8cf01faa6a0296dd4bbdb929db4f454ea0. Revision: 46f12389d4d4833fed51640a48cf938516433106.
3944: async fn handle_mutation(handle: &EngineHandle, m: Mutation) -> serde_json::Value {
3945: // Serialize the write pipeline: one mutation at a time across every
3946: // connection. Held across the whole saga and rebuild, this enforces the
3947: // single-threaded model the intent marker and crash recovery rely on. Reads
3948: // and subscriptions never take it.
3949: let _write = handle.write_lock().lock().await;
3950: // Mutating while Deriving would race the initial build; the consumer
3951: // waits for ready like it does for reads.
3952: if handle.version().is_none() {
3953: return serde_json::to_value(Response::not_ready()).expect("response serializes");
3954: }
3955: match m {
3956: Mutation::WriteFile(args) => {
3957: let write_path = match resolve_mutation_target(handle, &args.path) {
3958: Ok(t) => t,
3959: Err(frame) => return frame,
au-engine: crates/au-engine/src/serve.rs, lines 3980–3987
Whole-blob SHA-256: dcd7f1ac12dce9066078741081c8cc8cf01faa6a0296dd4bbdb929db4f454ea0. Revision: 46f12389d4d4833fed51640a48cf938516433106.
3980: let outcome = tokio::task::spawn_blocking(move || {
3981: let ((written, mixins), commits) =
3982: apply_saga(&ShellGit, &marker_dir, &plan, summary, false, || {
3983: let written = crate::mutate::write_file(
3984: &blocking_path,
3985: &args.content,
3986: args.expected_hash.as_deref(),
3987: )?;
au-engine: crates/au-engine/src/mutate.rs, lines 105–139
Whole-blob SHA-256: dd84a3b3d9b264f8a558c1b011894318f2be321a14aab432d68b86d34df6583f. Revision: 46f12389d4d4833fed51640a48cf938516433106.
105: pub(crate) fn write_file(
106: target: &Path,
107: content: &str,
108: expected_hash: Option<&str>,
109: ) -> Result<ContentHash, MutationReject> {
110: if let Some(expected) = expected_hash {
111: match std::fs::read(target) {
112: Ok(current) => {
113: let current_hex = hash_hex(ContentHash::of(¤t));
114: if current_hex != expected {
115: return Err(MutationReject {
116: message: format!(
117: "expected_hash mismatch: the file changed since it was read \
118: (current {current_hex})"
119: ),
120: detail: Some(serde_json::json!({ "current_hash": current_hex })),
121: });
122: }
123: }
124: Err(_) => {
125: return Err(MutationReject::new(
126: "expected_hash given but the file does not exist",
127: ));
128: }
129: }
130: }
131: if let Some(parent) = target.parent() {
132: if let Err(e) = std::fs::create_dir_all(parent) {
133: return Err(MutationReject::new(format!(
134: "cannot create parent directories: {e}"
135: )));
136: }
137: }
138: std::fs::write(target, content)
139: .map_err(|e| MutationReject::new(format!("cannot write {}: {e}", target.display())))?;
Namespaced-path probe
This is a manual manifest-routing/object-presence probe, not an implemented bundle resolver or a workflow trial. The rule tested is: a normalized relative POSIX path starts with an approved component name; strip that first segment, then look up the remaining committed blob at that component’s full pin. No fallback search.
| Case | Local observation or required rejection |
|---|---|
au-mcp/README.md |
Remaining path is a blob at the named pin; shared basenames are disambiguated |
au-mcp-sdk/README.md |
Remaining path is a blob at the named pin; shared basenames are disambiguated |
au-mcp-core/README.md |
Remaining path is a blob at the named pin; shared basenames are disambiguated |
au-mcp/../au-mcp-core/README.md |
Normalized-path predicate: False; reject before object lookup |
/au-mcp/README.md |
Normalized-path predicate: False; reject before object lookup |
au-mcp//README.md |
Normalized-path predicate: False; reject before object lookup |
au-mcp/./README.md |
Normalized-path predicate: False; reject before object lookup |
unknown/README.md |
Not a manifest member; reject, do not search sibling repositories |
au-mcp/missing-scope-probe.ts |
Object lookup below confirms absent blob; reject as nonexistent anchor |
Explicit revision 0000000000000000000000000000000000000000 for au-mcp/README.md |
Unequal to selected pin; reject before lookup even if the quote text could match |
au-mcp/README.md under the current single au-mcp source |
No such prefixed committed blob; the existing resolver does not supply namespace routing |
A read-only Python probe of the current GitPin.attribution_error rejected explicit revision 0000000000000000000000000000000000000000 for README.md with attribution uses revision 0000000000000000000000000000000000000000, expected 3ddc3b62ed0b339a89a57f11026676df18d3b2eb. It also rejected an au-mcp-core blob URL against the au-mcp pin with attribution uses repository https://github.com/arsumbris/au-mcp-core, expected https://github.com/arsumbris/au-mcp. Path-only attribution was accepted, and the current is_normalized_relative rejected all four escape/redundancy cases above. This does not test bundle dispatch. The prefixed-path row is an object-presence check, not a workflow acceptance result. Missing approved repository objects must leave affected quotations unverified during retained reading and prevent live acceptance/publication, not resolve against a different member. Symlink blobs must never authorize following another filesystem root.