@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 +124 -12
- package/package.json +8 -3
- package/src/generation-entry.js +3 -85
- package/src/generation-protocol.js +2 -44
- package/src/generation-runtime.js +94 -0
- package/src/generation-thread-entry.js +8 -0
- package/src/generation.js +64 -20
- package/src/main.js +42 -10
- package/src/server.js +44 -22
- package/src/stdio-output.js +42 -0
- package/src/ui-capability.js +49 -1
- package/src/worker-entry.js +2 -2
- package/src/worker.js +53 -7
- package/ui/cockpit.html +31 -31
- package/ui/decision-manifest.json +3 -3
- package/ui/decision.html +36 -36
- package/ui/manifest.json +4 -4
package/README.md
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
# @agdf/mcp-server
|
|
2
2
|
|
|
3
|
-
Local STDIO MCP adapter
|
|
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
|
-
|
|
8
|
-
|
|
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.
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|
package/src/generation-entry.js
CHANGED
|
@@ -1,86 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Explicit process adapter retained for undeclared/direct runtimes.
|
|
2
2
|
import process from 'node:process';
|
|
3
|
-
import {
|
|
4
|
-
|
|
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
|
-
|
|
2
|
-
export
|
|
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() } });
|