@agdf/mcp-server 0.20.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,22 @@
1
1
  # @agdf/mcp-server
2
2
 
3
- Local STDIO MCP adapter for AGDF dispatch and control inspection.
3
+ Local STDIO MCP adapter.
4
4
 
5
- MCP tools: `agdf_dispatch`, `agdf_inspect`, `agdf_prepare`, `agdf_record`, `agdf_approve`, `agdf_revise`, `agdf_closeout`.
5
+ MCP tools: `agdf_dispatch`, `agdf_inspect`, `agdf_project`, `agdf_run`, `agdf_prepare`, `agdf_record`, `agdf_approve`, `agdf_revise`, `agdf_closeout`.
6
6
 
7
- The bounded write groups, selected contract/status reads, original-operation recovery and
8
- focused app-only decision facade are described in [Core and MCP command flow](../../docs/architecture/core-mcp-command-flow.md).
7
+ [Integration guide](../../docs/architecture/09-mcp-integrationsrichtlinien.md).
8
+ [Command flow](../../docs/architecture/core-mcp-command-flow.md): bounded writes,
9
+ contract/status reads, original-operation recovery and the app-only decision facade.
9
10
  Host tool permission is separate from a deliberate gate decision. The source candidate's
10
11
  click actions remain disabled while native app-only mediation is unqualified.
11
12
 
13
+ `agdf_closeout` also ends active runs as `superseded` or `abandoned`. Kind `closure.preview` takes
14
+ `{"entries":[{"run_id","outcome","reason","successor_run"|"successor_reference"}]}` and returns the
15
+ preview text with the phrase `Close runs: <preview_id>`; kind `closure.apply` takes `preview_id`
16
+ and the owner's verbatim `confirmation`. Both use a target-bound envelope without `run_id` or
17
+ `expected_revision_id`; every other `agdf_closeout` kind keeps the per-run envelope. The agent
18
+ forwards only the owner's reply and never supplies the phrase itself.
19
+
12
20
  `agdf_inspect` also exposes read-only draft preflight through `operation: artifact-readiness`:
13
21
 
14
22
  ```json
@@ -43,9 +51,59 @@ Keep the original document revision, including for historical reads. Catalog pag
43
51
  16 candidates; scans stop at 256 records, 1 MiB of record bytes or 1024 directory entries. Catalog,
44
52
  document and preview cursors are not interchangeable. Prepared records prove no human approval.
45
53
 
54
+ For a known task target, always send `primary_target` together with its actual `target_source`:
55
+ `explicit_target` for a named target, `continued_target` for a previously confirmed target, or
56
+ `current_repository` for an unambiguous request about the current repository. `working_directory`
57
+ alone never selects it. A bound `gate-check` returns `navigation.read_context`: copy those explicit
58
+ selectors into later status reads, adding `operation: "gate-check"`. This is a reusable locator,
59
+ not a connection session or permission; target and Run checks run again on every call. Never add
60
+ `gate` or `expected_revision_id` to a status read. For document reads, copy the exact returned
61
+ `read_arguments`, including the original revision. A uniquely selected document is the first
62
+ `navigation.next_calls` entry, ahead of optional operation help. Follow every document page before
63
+ that help. An ambiguous or incomplete catalog still requires selection and never chooses a record.
64
+
65
+ Input-validation hints describe missing fields for the selected operation and invalid Run-ID
66
+ syntax. They neither normalize IDs nor fill targets, replace revisions, replay writes or grant
67
+ approval. The original SDK and Core validators continue to reject the original invalid request.
68
+ For malformed status/document reads with individually valid explicit locators, the hint can include
69
+ a complete new `gate-check` read. This suggestion is length-bounded, excludes write/revision/document
70
+ selectors and is validated again when called. It never fabricates the missing presentation ID.
71
+ Read the returned catalog and exact document arguments; `authoring-inputs` is not a substitute for
72
+ reading the selected prepared document. A wrong operation group gets a correction only when the
73
+ operation kind has exactly one registered owner; ambiguous kinds require contract discovery.
74
+
46
75
  Dispatch follows the canonical skill-dispatch contract;
47
76
  inspect exposes read-only doctor, gate-check, delivery-map and contract operations.
48
77
 
78
+ `agdf_inspect/project-state` observes the confirmed existing Git project's setup without a Run.
79
+ Deliberate `agdf_project/project.initialize` uses the shared Core initializer and fixed versioned
80
+ plan, preserving authored initialized governance. It creates no Run, approval, installation,
81
+ permission or Git effect and exposes no force/repair/arbitrary writer. Original request recovery
82
+ uses `operation-status` module `project.initialize/<original UUID>`, distinct from Run creation.
83
+ Unknown/unowned partial states remain blocked. Successful setup returns creation help.
84
+
85
+ `agdf_run/run.create` creates an unapproved Run from a semantic title and original operation UUID;
86
+ pre-Run `operation-status` resolves a lost response without inventing a second Run.
87
+ `authoring-inputs` returns versioned templates and exact sources with bounded cursors.
88
+ `agdf_record/material.stage` transfers at most 16 KiB per part and 128 KiB per file, then seals
89
+ Core-owned references for existing typed writers, including reviewed formal artefacts, unapproved
90
+ UR replacement, Verified Change and closeout. Code edits, actual tests and Git execution remain
91
+ external work; none of these operations grants approval.
92
+
93
+ MCP `doctor` observes an exact selected Run without enumerating unrelated histories. All-active
94
+ diagnosis returns bounded pages and an opaque observation cursor; consume every `next_cursor`.
95
+ Only complete accounted observations have an overall domain status. Partial, stale, expired or
96
+ resource-limited reads never imply a clean pass. Read failures report elapsed/unmeasured timing
97
+ and read recovery separately from uncertain writes and their original operation receipts.
98
+
99
+ Preparation opens no UI. `presentation.prepare` freezes one Markdown decision document;
100
+ `presentation.deliver` chooses its primary delivery once. The ordinary route returns the whole
101
+ Markdown. Optional `agdf_decision_view` opens a non-primary read-only preview. UI primary delivery
102
+ requires actual bound current-surface confirmation and gives Chat only a short pointer; action
103
+ qualification remains separate. The current SDK adapter cannot establish that surface identity,
104
+ so capability/qualification alone always keeps Markdown as the primary route. Replay adds no
105
+ second question; explicit fallback retires a primary view before returning the same Markdown.
106
+
49
107
  The shared Core continuation service may record one previously sealed relationship correction
50
108
  for an authorized bound `continue_delivery` request, then route the same run afresh. The routing
51
109
  service itself remains read-only. Accordingly, `agdf_dispatch` declares `readOnlyHint: false`;
@@ -53,8 +111,7 @@ service itself remains read-only. Accordingly, `agdf_dispatch` declares `readOnl
53
111
 
54
112
  > [!IMPORTANT]
55
113
  > This package documents the repository development build. The previously published AGDF 0.14.5 release
56
- > did not include the MCP support described here. Repository assemblies and local plugin builds still
57
- > carry the canonical version `0.14.5`, but can contain these newer MCP features. The version alone
114
+ > did not include the MCP support described here. Current assemblies and local plugin builds take their version from the canonical package manifests. The version alone
58
115
  > does not identify the payload: verify the assembly or installed runtime digests and the loaded session.
59
116
  > These development instructions do not establish what a current npm `@latest` resolution contains.
60
117
 
@@ -70,12 +127,37 @@ MCP transaction retain separate results and recovery. A verified plugin is never
70
127
  because MCP setup fails. Marketplace-only installation has no CLI callback and therefore remains
71
128
  plugin-only.
72
129
 
73
- `agdf_dispatch` requires `presentation_language` as one well-formed BCP 47 tag. The host selects it
74
- from the latest natural-language request: an explicit response-language instruction wins, otherwise
75
- the dominant language is used, with `en` for mixed or ambiguous input. Missing or invalid input is
76
- rejected by the MCP schema before governance evaluation. A valid unsupported tag is accepted and
77
- renders through the complete English locale pack. The production tests exercise this contract
78
- separately for protocol versions `2025-11-25` and `2026-07-28`.
130
+ The nine regular MCP tools may omit `presentation_language`; the adapter resolves a valid explicit
131
+ request override, then the confirmed project's `.agdf/control/config.json`, then the versioned
132
+ `plugins/agdf/meta/agdf-defaults.json`. Shipped preferences are `chat_language: en`,
133
+ `artifact_language: en`, `presentation_route: markdown`, and `gate_check_variant: null`.
134
+ The existing complete project configuration remains the only project override file. The last two
135
+ settings are optional siblings of its language fields. Explicit invalid input and present invalid
136
+ recognized settings are errors; defaults never activate or repair missing/incomplete control.
137
+
138
+ Set an explicit BCP 47 language when the current request differs from the configured language.
139
+ A valid unsupported presentation tag selects one complete English pack; malformed tags/resources
140
+ fail. Presentation overrides never change artefact language or approved material. Existing setup
141
+ locale selection and stored project values remain; normal calls and package generation write no config.
142
+ The seven command groups also allow omitted `schema_version` and `assurance`; their fixed values
143
+ come from the contracts, not configuration or independent human proof. `presentation.deliver` may
144
+ omit route; only inspect `gate-check` may inherit the configured variant. UI/approval owners stay unchanged.
145
+
146
+ `working_directory` may be omitted only with private trustworthy invocation context. Current native
147
+ adapters have no evidenced task-directory source and return `working_directory_required` on omission;
148
+ supply the absolute execution directory explicitly. Neither target nor plugin/process directory is a substitute.
149
+ All target/Run/revision/operation/presentation/digest/reply bindings remain explicit. The SDK accepts
150
+ the public omission schema; existing Core owners receive and validate the complete effective envelope.
151
+ Existing explicit clients remain usable. Existing inspect/help reports expose effective values and sources
152
+ without a mandatory extra lookup before each ordinary call.
153
+
154
+ Existing family records retain original resolution in their versioned `resolved/1` extension. New readers
155
+ validate its original family version and original logical/effective request digests. Unchanged complete-envelope
156
+ digest and existing family recovery stay authoritative. A same-UUID retry restores original values after
157
+ configuration changes; another logical request conflicts. Prepared language/material/identity remain immutable.
158
+ Legacy records are read unchanged; old binaries can reject new record versions. No shadow ledger or automatic
159
+ history/config migration is introduced. Repository/SDK, native CLI transport, installed plugin and human UI
160
+ acceptance require separate evidence; a smaller definition or green build alone proves no task-efficiency gain.
79
161
 
80
162
  ### Selected Cockpit draft authoring check
81
163
 
@@ -134,3 +216,33 @@ This follows the [MCP tool error contract](https://modelcontextprotocol.io/speci
134
216
  and [SDK error behavior](https://ts.sdk.modelcontextprotocol.io/v2/servers/errors).
135
217
  Local protocol, package and browser tests do not establish native host or agent
136
218
  performance; the unchanged ergonomics comparison records that evidence separately.
219
+
220
+
221
+ ### Owned thread execution
222
+
223
+ New packages declare `agdfExecutionModel: owner_threads_v1` and the exact owner-protocol
224
+ import/export. The existing launcher and detached retention owner remain OS processes.
225
+ That owner hosts one genuine SDK facade Worker and at most three counted generation
226
+ Workers. Per-request Workers keep their existing execution and lock owners. Public
227
+ STDIO uses native streams and backpressure; generation output cannot write MCP STDOUT.
228
+ One entry-owned SDK output queue serializes actual writes, bounding queued frames/bytes
229
+ without lowering existing per-result or UI resource limits. A blocked reader therefore
230
+ does not create a parallel drain/error listener for every response.
231
+ The private channel accepts bounded finite JSON with exact fields, a per-connection
232
+ token/epoch, publication identity and original operation identity. It limits pending
233
+ frames and bytes, and requalifies replacement candidates through the existing immutable
234
+ publisher. Historical packages retain their declared process path and digest entry sets.
235
+
236
+ Executable claims bind once to actual native Worker objects. Successful actual exit
237
+ and a complete close acknowledgement are both required for release. Facade loss drains
238
+ surviving work without replay; unconfirmed exits, uncertain effects and nested locks
239
+ remain protected. Fatal failure of the shared owner process can lose all its Workers;
240
+ original operation receipts and retained claims remain the recovery evidence. No PID
241
+ scan, claim garbage collection or timeout alone establishes safe termination.
242
+
243
+ Repository tests exercise both actual STDIO protocols, replacement, bounded channel
244
+ admission and real write/lock interruption in disposable, fully rehashed fault candidates.
245
+ They do not qualify an installed host or another OS. The fixed performance test selects
246
+ the actual owned plugin launcher with 20/20 samples and p95 limits of 2500 ms cold /
247
+ 1000 ms warm. Performance acceptance and native qualification are separate
248
+ required evidence, not consequences of the topology declaration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agdf/mcp-server",
3
- "version": "0.20.1",
3
+ "version": "0.30.0",
4
4
  "description": "Local MCP adapter for version-matched AGDF dispatch and control inspection.",
5
5
  "type": "module",
6
6
  "files": [
@@ -16,10 +16,14 @@
16
16
  "exports": {
17
17
  ".": "./src/server.js"
18
18
  },
19
+ "imports": {
20
+ "#agdf-dispatch-worker": "create-agdf/mcp-worker-runtime",
21
+ "#agdf-owner-protocol": "create-agdf/mcp-owner-protocol"
22
+ },
19
23
  "scripts": {},
20
24
  "dependencies": {
21
25
  "@modelcontextprotocol/server": "2.0.0",
22
- "create-agdf": "0.20.1"
26
+ "create-agdf": "0.30.0"
23
27
  },
24
28
  "engines": {
25
29
  "node": ">=22"
@@ -39,5 +43,6 @@
39
43
  "url": "git+https://github.com/ArndtGold/ai-native-governance-delivery-framework.git",
40
44
  "directory": "packages/mcp-server"
41
45
  },
42
- "homepage": "https://agdf.iself.eu"
46
+ "homepage": "https://agdf.iself.eu",
47
+ "agdfExecutionModel": "owner_threads_v1"
43
48
  }
@@ -1,86 +1,4 @@
1
- // A directly owned private child. It never reads or writes public MCP STDIO.
1
+ // Explicit process adapter retained for undeclared/direct runtimes.
2
2
  import process from 'node:process';
3
- import { createAgdfWorkerExecutor } from './server.js';
4
- import { loadCockpitResource } from './cockpit-resource.js';
5
- import { generationMessage, validGenerationMessage, generationManifest, generationFailureReason } from './generation-protocol.js';
6
- import { createMcpDispatchRuntime, createMcpDecisionRuntime, createMcpCockpitRuntime, DECISION_UI_URI,
7
- inspectMcpDispatcherRuntime, releaseAbandonedOwnedLock } from 'create-agdf/mcp-dispatch-runtime';
8
- const epoch = process.argv[2], generation = Number(process.argv[3]);
9
- let runtime, executor, starting = false, closing = false, inflight = 0, serverDigest, sdkDigest, uiDigest;
10
- const observed = new Set(), unknown = new Set();
11
- function status() {
12
- const owned = runtime?.inspectOwnership?.() ?? { views: 0 };
13
- return { ...owned, inflight: inflight + (starting ? 1 : 0) + (owned.inflight ?? 0),
14
- executor: executor?.active ? 1 : 0, uncertain: unknown.size + (owned.uncertain ?? 0), closing };
15
- }
16
- function send(input, kind, payload) {
17
- const reply = generationMessage(epoch, generation, kind, payload, {}, input.original_operation_id, input.request_id);
18
- if (validGenerationMessage(reply, { epoch, generation, reply: true })) process.send?.(reply);
19
- else process.send?.(generationMessage(epoch, generation, 'failure', { reason: 'generation_result_limit' }, {}, input.original_operation_id, input.request_id));
20
- }
21
- process.on('message', async input => {
22
- if (!validGenerationMessage(input, { epoch, generation }) || observed.has(input.request_id)) return;
23
- if (observed.size >= 8192) { send(input, 'failure', { reason: 'generation_request_capacity' }); return; }
24
- observed.add(input.request_id);
25
- try {
26
- if (input.kind === 'qualify') {
27
- if (runtime || starting || closing) throw Error('generation_already_initialized');
28
- starting = true;
29
- const { surface, commandGroups, cockpitDir, cockpitMode } = input.payload;
30
- const inspected = inspectMcpDispatcherRuntime();
31
- if (inspected.provenanceStatus !== 'matched') throw Error('runtime_provenance_invalid');
32
- runtime = cockpitDir || cockpitMode ? await createMcpCockpitRuntime({ surface, cockpitDir, cockpitMode, inspected })
33
- : createMcpDispatchRuntime({ surface, commandGroups, inspected });
34
- if (!cockpitDir && !cockpitMode && surface === 'codex') {
35
- const ui = { uri: DECISION_UI_URI, mimeType: 'text/html;profile=mcp-app', limit: 4 * 1024 ** 2, kind: 'decision' };
36
- ui.digest = loadCockpitResource(ui).digest;
37
- runtime = await createMcpDecisionRuntime({ runtime, ui });
38
- }
39
- if (runtime.ui) uiDigest = loadCockpitResource(runtime.ui).digest;
40
- // Inspect reports package-owned immutable identities, not caller evidence.
41
- sdkDigest = inspected.generationIdentity.sdk_digest; serverDigest = inspected.generationIdentity.server_digest;
42
- executor = runtime.mode === 'cockpit' ? null : createAgdfWorkerExecutor(runtime, { releaseOwnedLock: releaseAbandonedOwnedLock });
43
- const support = { ui: Boolean(runtime.ui), app_mediation: runtime.mode === 'decision'
44
- && runtime.trustedContext.decisionQualification(input.context.client, uiDigest) === true };
45
- starting = false;
46
- send(input, 'qualified', { manifest: generationManifest(runtime, uiDigest, sdkDigest),
47
- producer: { server_digest: serverDigest, dispatcher_digest: inspected.generationIdentity.dispatcher_digest, sdk_digest: sdkDigest }, support });
48
- } else if (input.kind === 'session_inspect') {
49
- send(input, 'status', status());
50
- } else if (input.kind === 'shutdown') {
51
- if (closing || starting || inflight || unknown.size) throw Error('generation_work_protected');
52
- closing = true;
53
- await executor?.close(); await runtime?.close?.();
54
- const remaining = status();
55
- if (remaining.views || remaining.inflight || remaining.executor || remaining.uncertain) throw Error('generation_cleanup_unconfirmed');
56
- send(input, 'closed', remaining);
57
- process.disconnect(); // Natural exit, no kill, after all owned work is closed.
58
- } else {
59
- if (!runtime || closing) throw Error('generation_unavailable');
60
- const { name, arguments: args } = input.payload, tool = runtime.tool?.(name) ?? runtime.tools.find(t => t.name === name);
61
- if (!tool || tool.requiresUi && input.context.uiSupported !== true) throw Error('resource_denied');
62
- ++inflight;
63
- let result, phase = tool.parentOwned || !executor ? 'parse' : 'execute';
64
- try {
65
- if (tool.parentOwned || !executor) {
66
- const parsed = tool.parse(args); phase = 'execute';
67
- result = await tool.execute(parsed, undefined, input.context);
68
- } else result = await executor.execute(args, { toolName: name });
69
- if (runtime.postprocessToolResult) result = await runtime.postprocessToolResult(name, args, result, input.context);
70
- if (input.original_operation_id && result.outcome === 'effect_unknown') unknown.add(input.original_operation_id);
71
- if (name === 'agdf_inspect' && args.operation === 'operation-status' && result.report?.status === 'recorded'
72
- && result.report.receipt?.operation_id === args.module) unknown.delete(args.module);
73
- send(input, 'result', runtime.projectToolResult(name, args, result));
74
- } catch (error) {
75
- const facts = runtime.normalizeFailure(error, { phase });
76
- if (input.original_operation_id && facts.dispatch !== 'not_dispatched') unknown.add(input.original_operation_id);
77
- send(input, 'result', runtime.projectToolResult(name, args, runtime.failure(facts.code, name, args, facts), facts));
78
- } finally { --inflight; }
79
- }
80
- } catch (error) { if (input.kind === 'qualify') starting = false; send(input, 'failure', { reason: generationFailureReason(error) }); }
81
- });
82
- process.on('disconnect', () => {
83
- // Loss of the facade does not cancel or replay business effects. Timers and
84
- // existing worker/view owners retain their bounds; no process-wide force kill.
85
- if (!closing) { void executor?.close().catch(() => {}); void runtime?.close?.().catch(() => {}); }
86
- });
3
+ import { startGenerationRuntime } from './generation-runtime.js';
4
+ startGenerationRuntime({ epoch: process.argv[2], generation: Number(process.argv[3]), channel: { on: (...args) => process.on(...args), send: value => process.send?.(value), close: () => process.disconnect() } });
@@ -1,44 +1,2 @@
1
- import { createHash } from 'node:crypto';
2
- export const GENERATION_LIMITS = Object.freeze({ slots: 3, input: 65536, result: 1048576, qualify: 30000, close: 10000, receipts: 256, routes: 4096 });
3
- const uuid = value => typeof value === 'string' && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/u.test(value);
4
- const fields = ['schema_version', 'owner_epoch', 'generation_id', 'request_id', 'kind', 'original_operation_id', 'payload', 'context'];
5
- export function validGenerationMessage(value, { epoch, generation, reply = false } = {}) {
6
- if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).sort().join() !== [...fields].sort().join()
7
- || value.schema_version !== '1' || value.owner_epoch !== epoch || value.generation_id !== generation
8
- || !uuid(value.request_id) || !(value.original_operation_id === null || uuid(value.original_operation_id))
9
- || !Number.isSafeInteger(value.generation_id) || value.generation_id < 1
10
- || !(reply ? ['qualified', 'result', 'status', 'closed', 'failure'] : ['qualify', 'invoke', 'session_inspect', 'shutdown']).includes(value.kind)
11
- || !value.context || typeof value.context !== 'object' || Array.isArray(value.context)) return false;
12
- try { return Buffer.byteLength(JSON.stringify(value)) <= (reply ? GENERATION_LIMITS.result : GENERATION_LIMITS.input); } catch { return false; }
13
- }
14
- export function generationMessage(epoch, generation, kind, payload, context = {}, original = null, requestId) {
15
- return { schema_version: '1', owner_epoch: epoch, generation_id: generation, request_id: requestId,
16
- kind, original_operation_id: original, payload, context };
17
- }
18
- export function generationManifest(runtime, uiDigest, sdkDigest) {
19
- if (typeof runtime.toolErrorContract !== 'string' || !runtime.toolErrorContract
20
- || typeof runtime.projectToolResult !== 'function' || typeof runtime.consumeToolResult !== 'function'
21
- || typeof runtime.normalizeFailure !== 'function') throw Error('generation_manifest_incompatible');
22
- // Stable transport shape, never business eligibility or a human-origin proof.
23
- return { schema_version: '1', mode: runtime.mode ?? 'headless', sdk_digest: sdkDigest,
24
- protocol: 'mcp-sdk/2:stdio-legacy-and-modern', tool_error_contract: runtime.toolErrorContract,
25
- tools: runtime.tools.map(tool => ({ name: tool.name, definition: tool.definition })),
26
- resource: runtime.ui ? { uri: runtime.ui.uri, mimeType: runtime.ui.mimeType, digest: uiDigest } : null,
27
- command_groups: [...(runtime.trustedContext.commandGroups ?? [])] };
28
- }
29
- export const manifestDigest = manifest => createHash('sha256').update(JSON.stringify(manifest)).digest('hex');
30
-
31
- const FAILURE_REASONS = new Set([
32
- 'generation_unavailable', 'generation_input_limit', 'generation_response_unknown', 'generation_connection_lost',
33
- 'generation_capacity_protected', 'generation_manifest_incompatible', 'generation_result_limit',
34
- 'generation_request_capacity', 'generation_already_initialized', 'generation_work_protected',
35
- 'generation_cleanup_unconfirmed', 'resource_denied', 'runtime_provenance_invalid', 'runtime_version_mismatch',
36
- 'dispatch_busy', 'dispatch_cancelled', 'dispatch_timeout', 'dispatch_worker_failed', 'dispatch_worker_termination_unconfirmed',
37
- 'runtime_publication_unowned', 'runtime_candidate_manifest_invalid', 'runtime_candidate_unowned', 'runtime_candidate_digest_mismatch',
38
- 'AGDF_MCP_EXECUTABLE_CLAIM_INVALID', 'AGDF_MCP_EXECUTABLE_CLAIM_CONTENDED',
39
- 'AGDF_MCP_EXECUTABLE_CLAIM_LOCK_UNCONFIRMED', 'AGDF_MCP_PUBLICATION_DEADLINE_EXCEEDED',
40
- ]);
41
- export function generationFailureReason(error) {
42
- const reason = error?.code ?? error?.message;
43
- return FAILURE_REASONS.has(reason) ? reason : 'generation_runtime_failed';
44
- }
1
+ // One SDK-free protocol owner, shared with the actual lifecycle owner.
2
+ export * from '#agdf-owner-protocol';
@@ -0,0 +1,94 @@
1
+ // A directly owned private child. It never reads or writes public MCP STDIO.
2
+ import { createAgdfWorkerExecutor } from './worker.js';
3
+ import { loadCockpitResource } from './cockpit-resource.js';
4
+ import { generationMessage, validGenerationMessage, generationManifest, generationFailureReason } from './generation-protocol.js';
5
+ import { createMcpDispatchRuntime, createMcpDecisionRuntime, createMcpCockpitRuntime, DECISION_UI_URI,
6
+ inspectMcpDispatcherRuntime, releaseAbandonedOwnedLock } from 'create-agdf/mcp-worker-runtime';
7
+ export function startGenerationRuntime({ epoch, generation, channel }) {
8
+ let runtime, executor, starting = false, closing = false, inflight = 0, serverDigest, sdkDigest, uiDigest;
9
+ const observed = new Set(), unknown = new Set();
10
+ function status() {
11
+ const owned = runtime?.inspectOwnership?.() ?? { views: 0 };
12
+ return { ...owned, inflight: inflight + (starting ? 1 : 0) + (owned.inflight ?? 0),
13
+ executor: executor?.active ? 1 : 0, uncertain: unknown.size + (owned.uncertain ?? 0), closing };
14
+ }
15
+ function send(input, kind, payload) {
16
+ const reply = generationMessage(epoch, generation, kind, payload, {}, input.original_operation_id, input.request_id);
17
+ if (validGenerationMessage(reply, { epoch, generation, reply: true })) channel.send(reply);
18
+ else channel.send(generationMessage(epoch, generation, 'failure', { reason: 'generation_result_limit' }, {}, input.original_operation_id, input.request_id));
19
+ }
20
+ channel.on('message', async input => {
21
+ if (!validGenerationMessage(input, { epoch, generation }) || observed.has(input.request_id)) return;
22
+ if (observed.size >= 8192) { send(input, 'failure', { reason: 'generation_request_capacity' }); return; }
23
+ observed.add(input.request_id);
24
+ try {
25
+ if (input.kind === 'qualify') {
26
+ if (runtime || starting || closing) throw Error('generation_already_initialized');
27
+ starting = true;
28
+ const { surface, commandGroups, cockpitDir, cockpitMode } = input.payload;
29
+ const inspected = inspectMcpDispatcherRuntime();
30
+ if (inspected.provenanceStatus !== 'matched') throw Error('runtime_provenance_invalid');
31
+ runtime = cockpitDir || cockpitMode ? await createMcpCockpitRuntime({ surface, cockpitDir, cockpitMode, inspected })
32
+ : createMcpDispatchRuntime({ surface, commandGroups, inspected });
33
+ if (!cockpitDir && !cockpitMode && surface === 'codex') {
34
+ const ui = { uri: DECISION_UI_URI, mimeType: 'text/html;profile=mcp-app', limit: 4 * 1024 ** 2, kind: 'decision' };
35
+ ui.digest = loadCockpitResource(ui).digest;
36
+ runtime = await createMcpDecisionRuntime({ runtime, ui });
37
+ }
38
+ if (runtime.ui) uiDigest = loadCockpitResource(runtime.ui).digest;
39
+ // Inspect reports package-owned immutable identities, not caller evidence.
40
+ sdkDigest = inspected.generationIdentity.sdk_digest; serverDigest = inspected.generationIdentity.server_digest;
41
+ executor = runtime.mode === 'cockpit' ? null : createAgdfWorkerExecutor(runtime, { releaseOwnedLock: releaseAbandonedOwnedLock });
42
+ const support = { ui: Boolean(runtime.ui), app_mediation: runtime.mode === 'decision'
43
+ && runtime.trustedContext.decisionQualification(input.context.client, uiDigest) === true };
44
+ starting = false;
45
+ send(input, 'qualified', { manifest: generationManifest(runtime, uiDigest, sdkDigest),
46
+ producer: { server_digest: serverDigest, dispatcher_digest: inspected.generationIdentity.dispatcher_digest, sdk_digest: sdkDigest }, support });
47
+ } else if (input.kind === 'session_inspect') {
48
+ send(input, 'status', status());
49
+ } else if (input.kind === 'shutdown') {
50
+ if (closing || starting || inflight || unknown.size) throw Error('generation_work_protected');
51
+ closing = true;
52
+ await executor?.close(); await runtime?.close?.();
53
+ const remaining = status();
54
+ if (remaining.views || remaining.inflight || remaining.executor || remaining.uncertain) throw Error('generation_cleanup_unconfirmed');
55
+ send(input, 'closed', remaining);
56
+ channel.close(); // Natural exit, no kill, after all owned work is closed.
57
+ } else {
58
+ if (!runtime || closing) throw Error('generation_unavailable');
59
+ const { name } = input.payload;
60
+ let args = input.payload.arguments;
61
+ const tool = runtime.tool?.(name) ?? runtime.tools.find(t => t.name === name);
62
+ if (!tool || tool.requiresUi && input.context.uiSupported !== true) throw Error('resource_denied');
63
+ ++inflight;
64
+ const parentOwned = typeof tool.parentOwned === 'function' ? tool.parentOwned(args) : tool.parentOwned;
65
+ const started = performance.now();
66
+ let result, phase = parentOwned || !executor ? 'parse' : 'execute';
67
+ try {
68
+ phase = 'parse';
69
+ args = runtime.prepareInput?.(name, args) ?? args;
70
+ phase = parentOwned || !executor ? 'parse' : 'execute';
71
+ if (parentOwned || !executor) {
72
+ const parsed = tool.parse(args); phase = 'execute';
73
+ result = await tool.execute(parsed, undefined, input.context);
74
+ } else result = await executor.execute(args, { toolName: name, trusted: runtime.prepareDispatch?.(name, args) });
75
+ if (runtime.postprocessToolResult) result = await runtime.postprocessToolResult(name, args, result, input.context);
76
+ if (input.original_operation_id && result.outcome === 'effect_unknown') unknown.add(input.original_operation_id);
77
+ if (name === 'agdf_inspect' && args.operation === 'operation-status' && result.report?.status === 'recorded'
78
+ && (result.report.receipt?.operation_id ?? result.report.receipt?.operation?.operation_id) === args.module.split('/').at(-1)) unknown.delete(args.module.split('/').at(-1));
79
+ send(input, 'result', runtime.projectToolResult(name, args, result));
80
+ } catch (error) {
81
+ const facts = runtime.normalizeFailure(error, { phase });
82
+ if (input.original_operation_id && facts.dispatch !== 'not_dispatched') unknown.add(input.original_operation_id);
83
+ send(input, 'result', runtime.projectToolResult(name, args, runtime.failure(facts.code, name, args, { ...facts, elapsed_ms: performance.now() - started }), facts));
84
+ } finally { --inflight; }
85
+ }
86
+ } catch (error) { if (input.kind === 'qualify') starting = false; send(input, 'failure', { reason: generationFailureReason(error) }); }
87
+ });
88
+ channel.on('disconnect', () => {
89
+ // Loss of the facade does not cancel or replay business effects. Timers and
90
+ // existing worker/view owners retain their bounds; no process-wide force kill.
91
+ if (!closing) { void executor?.close().catch(() => {}); void runtime?.close?.().catch(() => {}); }
92
+ });
93
+
94
+ }
@@ -0,0 +1,8 @@
1
+ // Genuine owner-hosted Worker; no public STDIO and no process impersonation.
2
+ import { parentPort, workerData } from 'node:worker_threads';
3
+ import { startGenerationRuntime } from './generation-runtime.js';
4
+ if (!parentPort) throw Error('generation_unavailable');
5
+ startGenerationRuntime({ epoch: workerData.epoch, generation: workerData.generation,
6
+ channel: { on: (kind, listener) => parentPort.on(kind === 'disconnect' ? 'close' : kind,
7
+ kind === 'message' ? wire => { try { listener(JSON.parse(wire)); } catch {} } : listener),
8
+ send: value => parentPort.postMessage(JSON.stringify(value)), close: () => parentPort.close() } });