@agdf/mcp-server 0.20.2 → 0.31.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 +143 -7
- package/package.json +8 -3
- package/src/generation-entry.js +3 -85
- package/src/generation-protocol.js +2 -44
- package/src/generation-runtime.js +103 -0
- package/src/generation-thread-entry.js +8 -0
- package/src/generation.js +64 -20
- package/src/main.js +43 -10
- package/src/next-action.js +227 -0
- package/src/prepared-call-transport.js +306 -0
- package/src/safe-read-continuation.js +62 -0
- package/src/server.js +159 -46
- package/src/stdio-output.js +42 -0
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
7
|
[Integration guide](../../docs/architecture/09-mcp-integrationsrichtlinien.md).
|
|
8
8
|
[Command flow](../../docs/architecture/core-mcp-command-flow.md): bounded writes,
|
|
@@ -10,6 +10,13 @@ contract/status reads, original-operation recovery and the app-only decision fac
|
|
|
10
10
|
Host tool permission is separate from a deliberate gate decision. The source candidate's
|
|
11
11
|
click actions remain disabled while native app-only mediation is unqualified.
|
|
12
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
|
+
|
|
13
20
|
`agdf_inspect` also exposes read-only draft preflight through `operation: artifact-readiness`:
|
|
14
21
|
|
|
15
22
|
```json
|
|
@@ -44,9 +51,69 @@ Keep the original document revision, including for historical reads. Catalog pag
|
|
|
44
51
|
16 candidates; scans stop at 256 records, 1 MiB of record bytes or 1024 directory entries. Catalog,
|
|
45
52
|
document and preview cursors are not interchangeable. Prepared records prove no human approval.
|
|
46
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
|
+
|
|
47
75
|
Dispatch follows the canonical skill-dispatch contract;
|
|
48
76
|
inspect exposes read-only doctor, gate-check, delivery-map and contract operations.
|
|
49
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
|
+
`authoring-inputs.material_call` supplies a bound form: fill its UTF-8 content and use
|
|
89
|
+
`agdf_record/material.stage` action `put` to seal files of at most 48 KiB each within the 64 KiB whole JSON request bound in one call.
|
|
90
|
+
The Core computes byte counts, material identities and digests. Larger files retain the bounded
|
|
91
|
+
multipart path (16 KiB per part, 128 KiB per file). Each multipart action has its own operation UUID;
|
|
92
|
+
only an identical retry reuses a UUID. Returned `recording_calls` provide the binding and material
|
|
93
|
+
reference; fill only the listed author/review fields. Ready `next_calls` are executable as supplied;
|
|
94
|
+
do not add intake/revision fields to a supplied `continue_delivery` dispatch.
|
|
95
|
+
Pass authored Markdown as UTF-8 string data through structured input. If a host uses JavaScript,
|
|
96
|
+
author with its file-editing tool and pass the UTF-8 file read as the content value; embedding
|
|
97
|
+
the whole document in a backtick literal makes Markdown syntax executable JavaScript syntax.
|
|
98
|
+
This guidance adds no server-side file reader and changes no byte or input limit.
|
|
99
|
+
These Core-owned references feed existing typed writers, including reviewed formal artefacts, unapproved
|
|
100
|
+
UR replacement, Verified Change and closeout. Code edits, actual tests and Git execution remain
|
|
101
|
+
external work; none of these operations grants approval.
|
|
102
|
+
|
|
103
|
+
MCP `doctor` observes an exact selected Run without enumerating unrelated histories. All-active
|
|
104
|
+
diagnosis returns bounded pages and an opaque observation cursor; consume every `next_cursor`.
|
|
105
|
+
Only complete accounted observations have an overall domain status. Partial, stale, expired or
|
|
106
|
+
resource-limited reads never imply a clean pass. Read failures report elapsed/unmeasured timing
|
|
107
|
+
and read recovery separately from uncertain writes and their original operation receipts.
|
|
108
|
+
|
|
109
|
+
Preparation opens no UI. `presentation.prepare` freezes one Markdown decision document;
|
|
110
|
+
`presentation.deliver` chooses its primary delivery once. The ordinary route returns the whole
|
|
111
|
+
Markdown. Optional `agdf_decision_view` opens a non-primary read-only preview. UI primary delivery
|
|
112
|
+
requires actual bound current-surface confirmation and gives Chat only a short pointer; action
|
|
113
|
+
qualification remains separate. The current SDK adapter cannot establish that surface identity,
|
|
114
|
+
so capability/qualification alone always keeps Markdown as the primary route. Replay adds no
|
|
115
|
+
second question; explicit fallback retires a primary view before returning the same Markdown.
|
|
116
|
+
|
|
50
117
|
The shared Core continuation service may record one previously sealed relationship correction
|
|
51
118
|
for an authorized bound `continue_delivery` request, then route the same run afresh. The routing
|
|
52
119
|
service itself remains read-only. Accordingly, `agdf_dispatch` declares `readOnlyHint: false`;
|
|
@@ -70,12 +137,37 @@ MCP transaction retain separate results and recovery. A verified plugin is never
|
|
|
70
137
|
because MCP setup fails. Marketplace-only installation has no CLI callback and therefore remains
|
|
71
138
|
plugin-only.
|
|
72
139
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
140
|
+
The nine regular MCP tools may omit `presentation_language`; the adapter resolves a valid explicit
|
|
141
|
+
request override, then the confirmed project's `.agdf/control/config.json`, then the versioned
|
|
142
|
+
`plugins/agdf/meta/agdf-defaults.json`. Shipped preferences are `chat_language: en`,
|
|
143
|
+
`artifact_language: en`, `presentation_route: markdown`, and `gate_check_variant: null`.
|
|
144
|
+
The existing complete project configuration remains the only project override file. The last two
|
|
145
|
+
settings are optional siblings of its language fields. Explicit invalid input and present invalid
|
|
146
|
+
recognized settings are errors; defaults never activate or repair missing/incomplete control.
|
|
147
|
+
|
|
148
|
+
Set an explicit BCP 47 language when the current request differs from the configured language.
|
|
149
|
+
A valid unsupported presentation tag selects one complete English pack; malformed tags/resources
|
|
150
|
+
fail. Presentation overrides never change artefact language or approved material. Existing setup
|
|
151
|
+
locale selection and stored project values remain; normal calls and package generation write no config.
|
|
152
|
+
The seven command groups also allow omitted `schema_version` and `assurance`; their fixed values
|
|
153
|
+
come from the contracts, not configuration or independent human proof. `presentation.deliver` may
|
|
154
|
+
omit route; only inspect `gate-check` may inherit the configured variant. UI/approval owners stay unchanged.
|
|
155
|
+
|
|
156
|
+
`working_directory` may be omitted only with private trustworthy invocation context. Current native
|
|
157
|
+
adapters have no evidenced task-directory source and return `working_directory_required` on omission;
|
|
158
|
+
supply the absolute execution directory explicitly. Neither target nor plugin/process directory is a substitute.
|
|
159
|
+
All target/Run/revision/operation/presentation/digest/reply bindings remain explicit. The SDK accepts
|
|
160
|
+
the public omission schema; existing Core owners receive and validate the complete effective envelope.
|
|
161
|
+
Existing explicit clients remain usable. Existing inspect/help reports expose effective values and sources
|
|
162
|
+
without a mandatory extra lookup before each ordinary call.
|
|
163
|
+
|
|
164
|
+
Existing family records retain original resolution in their versioned `resolved/1` extension. New readers
|
|
165
|
+
validate its original family version and original logical/effective request digests. Unchanged complete-envelope
|
|
166
|
+
digest and existing family recovery stay authoritative. A same-UUID retry restores original values after
|
|
167
|
+
configuration changes; another logical request conflicts. Prepared language/material/identity remain immutable.
|
|
168
|
+
Legacy records are read unchanged; old binaries can reject new record versions. No shadow ledger or automatic
|
|
169
|
+
history/config migration is introduced. Repository/SDK, native CLI transport, installed plugin and human UI
|
|
170
|
+
acceptance require separate evidence; a smaller definition or green build alone proves no task-efficiency gain.
|
|
79
171
|
|
|
80
172
|
### Selected Cockpit draft authoring check
|
|
81
173
|
|
|
@@ -134,3 +226,47 @@ This follows the [MCP tool error contract](https://modelcontextprotocol.io/speci
|
|
|
134
226
|
and [SDK error behavior](https://ts.sdk.modelcontextprotocol.io/v2/servers/errors).
|
|
135
227
|
Local protocol, package and browser tests do not establish native host or agent
|
|
136
228
|
performance; the unchanged ergonomics comparison records that evidence separately.
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
### Owned thread execution
|
|
232
|
+
|
|
233
|
+
New packages declare `agdfExecutionModel: owner_threads_v1` and the exact owner-protocol
|
|
234
|
+
import/export. The existing launcher and detached retention owner remain OS processes.
|
|
235
|
+
That owner hosts one genuine SDK facade Worker and at most three counted generation
|
|
236
|
+
Workers. Per-request Workers keep their existing execution and lock owners. Public
|
|
237
|
+
STDIO uses native streams and backpressure; generation output cannot write MCP STDOUT.
|
|
238
|
+
One entry-owned SDK output queue serializes actual writes, bounding queued frames/bytes
|
|
239
|
+
without lowering existing per-result or UI resource limits. A blocked reader therefore
|
|
240
|
+
does not create a parallel drain/error listener for every response.
|
|
241
|
+
The private channel accepts bounded finite JSON with exact fields, a per-connection
|
|
242
|
+
token/epoch, publication identity and original operation identity. It limits pending
|
|
243
|
+
frames and bytes, and requalifies replacement candidates through the existing immutable
|
|
244
|
+
publisher. Historical packages retain their declared process path and digest entry sets.
|
|
245
|
+
|
|
246
|
+
Executable claims bind once to actual native Worker objects. Successful actual exit
|
|
247
|
+
and a complete close acknowledgement are both required for release. Facade loss drains
|
|
248
|
+
surviving work without replay; unconfirmed exits, uncertain effects and nested locks
|
|
249
|
+
remain protected. Fatal failure of the shared owner process can lose all its Workers;
|
|
250
|
+
original operation receipts and retained claims remain the recovery evidence. No PID
|
|
251
|
+
scan, claim garbage collection or timeout alone establishes safe termination.
|
|
252
|
+
|
|
253
|
+
Repository tests exercise both actual STDIO protocols, replacement, bounded channel
|
|
254
|
+
admission and real write/lock interruption in disposable, fully rehashed fault candidates.
|
|
255
|
+
They do not qualify an installed host or another OS. The fixed performance test selects
|
|
256
|
+
the actual owned plugin launcher with 20/20 samples and p95 limits of 2500 ms cold /
|
|
257
|
+
1000 ms warm. Performance acceptance and native qualification are separate
|
|
258
|
+
required evidence, not consequences of the topology declaration.
|
|
259
|
+
|
|
260
|
+
The normal MCP path is `next_action.call`: either a ready call forwarded unchanged or a form with named `required_inputs` such as `content`, `title`, `reviewer`, `reviewed` or `response`. The adapter maps those names to the stored canonical payload fields. It retains the original target, Run, revision, permission group and operation UUID; it never fills authored content, review attestations or approval replies. The former competing `submit` form is absent. Raw requests remain accepted for compatibility, but the full original request is exposed only under the explicit `recovery_only` branch, with the exact field map and original UUID. Retain that branch and the actual submitted inputs for uncertain-outcome recovery; do not generate a replacement identity.
|
|
261
|
+
|
|
262
|
+
Verified owned plugin launchers retain offered templates in a private bounded journal under their existing MCP data root. The namespace includes the serving surface, version and server/dispatcher/SDK identity. A reconnect at that same identity can resolve the original reference without a new UUID or refreshed revision. The original 30-minute lifetime is never renewed. No submitted author/review/reply inputs or execution receipts are added to this journal; it is transport storage, not canonical Run State or authority. Direct transports without this verified owner remain connection-local. Private material buffers have their separate existing generation lifetime and still require exact semantic-byte recovery after generation loss.
|
|
263
|
+
|
|
264
|
+
Empty, malformed, unknown and known-expired references remain distinct. An unavailable live reference returns `prepared_call_unknown`, rather than blaming the authored command schema. Store failures are distinguished from missing references. A failed publication of a future reference never discards an already successful operation receipt; its exact original request remains in recovery. Every restored call still passes the current SDK, permission-group, Core and revision checks.
|
|
265
|
+
|
|
266
|
+
For a unique Core-supplied normal next step, the adapter may execute one exact `agdf_inspect` call for `authoring-inputs`, `gate-check` or `operation-contract` and include it as `next_read.result`. `next_action.context_path` names that already-loaded context. The original operation outcome and receipt remain intact if this separate read fails. No dispatch, write, presentation or approval is chained automatically; `agdf_dispatch` is not read-only. Source pagination remains explicit. Internal read attempts/failures are accounted separately from outer MCP invocations in native observations.
|
|
267
|
+
|
|
268
|
+
`authoring-inputs` supports `bundle:true` for bounded multi-document pages. Read every chunk and cursor without a duplicate writer-contract lookup. A retained first-page form is reusable only for the same exact manifest and an available connection reference; otherwise use the supplied first-page read. Page transport does not prove source review.
|
|
269
|
+
|
|
270
|
+
Prepared references also retain explicitly offered `agdf_inspect` reads, including source pages, contract help and original-operation status. Read inputs stay `{}`; the original SDK schema validates the complete supplied request before registration. Target, Run, revision, cursor and original status selection are never inferred or refreshed. The actual `next_read.tool/arguments` remain the record of an already executed read; its separate `next_read.call` is the exact prepared retry. Explicit recovery reads remain Raw for reconnect, and dispatch retains its original schema. No reference changes permission or bypasses canonical validation.
|
|
271
|
+
|
|
272
|
+
Bundled authoring reports use `document_bundle_v2`: `documents` contains complete text, `document_manifest` is metadata only, and `fragments` must be joined by identity and byte offsets across all cursors before editing. Missing content must never be coerced into a replacement string.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agdf/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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.31.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,103 @@
|
|
|
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
|
+
// Companion events (SD SDD-006): this generation executed the call, so it journals the final result
|
|
16
|
+
// once, before the result leaves; a failing journal never changes or delays the result.
|
|
17
|
+
function journal(name, args, projected) {
|
|
18
|
+
try { runtime.observeOutcome?.(name, args, projected); } catch { /* notification only */ }
|
|
19
|
+
}
|
|
20
|
+
function send(input, kind, payload) {
|
|
21
|
+
const reply = generationMessage(epoch, generation, kind, payload, {}, input.original_operation_id, input.request_id);
|
|
22
|
+
if (validGenerationMessage(reply, { epoch, generation, reply: true })) channel.send(reply);
|
|
23
|
+
else channel.send(generationMessage(epoch, generation, 'failure', { reason: 'generation_result_limit' }, {}, input.original_operation_id, input.request_id));
|
|
24
|
+
}
|
|
25
|
+
channel.on('message', async input => {
|
|
26
|
+
if (!validGenerationMessage(input, { epoch, generation }) || observed.has(input.request_id)) return;
|
|
27
|
+
if (observed.size >= 8192) { send(input, 'failure', { reason: 'generation_request_capacity' }); return; }
|
|
28
|
+
observed.add(input.request_id);
|
|
29
|
+
try {
|
|
30
|
+
if (input.kind === 'qualify') {
|
|
31
|
+
if (runtime || starting || closing) throw Error('generation_already_initialized');
|
|
32
|
+
starting = true;
|
|
33
|
+
const { surface, commandGroups, cockpitDir, cockpitMode } = input.payload;
|
|
34
|
+
const inspected = inspectMcpDispatcherRuntime();
|
|
35
|
+
if (inspected.provenanceStatus !== 'matched') throw Error('runtime_provenance_invalid');
|
|
36
|
+
runtime = cockpitDir || cockpitMode ? await createMcpCockpitRuntime({ surface, cockpitDir, cockpitMode, inspected })
|
|
37
|
+
: createMcpDispatchRuntime({ surface, commandGroups, inspected });
|
|
38
|
+
if (!cockpitDir && !cockpitMode && surface === 'codex') {
|
|
39
|
+
const ui = { uri: DECISION_UI_URI, mimeType: 'text/html;profile=mcp-app', limit: 4 * 1024 ** 2, kind: 'decision' };
|
|
40
|
+
ui.digest = loadCockpitResource(ui).digest;
|
|
41
|
+
runtime = await createMcpDecisionRuntime({ runtime, ui });
|
|
42
|
+
}
|
|
43
|
+
if (runtime.ui) uiDigest = loadCockpitResource(runtime.ui).digest;
|
|
44
|
+
// Inspect reports package-owned immutable identities, not caller evidence.
|
|
45
|
+
sdkDigest = inspected.generationIdentity.sdk_digest; serverDigest = inspected.generationIdentity.server_digest;
|
|
46
|
+
executor = runtime.mode === 'cockpit' ? null : createAgdfWorkerExecutor(runtime, { releaseOwnedLock: releaseAbandonedOwnedLock });
|
|
47
|
+
const support = { ui: Boolean(runtime.ui), app_mediation: runtime.mode === 'decision'
|
|
48
|
+
&& runtime.trustedContext.decisionQualification(input.context.client, uiDigest) === true };
|
|
49
|
+
starting = false;
|
|
50
|
+
send(input, 'qualified', { manifest: generationManifest(runtime, uiDigest, sdkDigest),
|
|
51
|
+
producer: { server_digest: serverDigest, dispatcher_digest: inspected.generationIdentity.dispatcher_digest, sdk_digest: sdkDigest }, support });
|
|
52
|
+
} else if (input.kind === 'session_inspect') {
|
|
53
|
+
send(input, 'status', status());
|
|
54
|
+
} else if (input.kind === 'shutdown') {
|
|
55
|
+
if (closing || starting || inflight || unknown.size) throw Error('generation_work_protected');
|
|
56
|
+
closing = true;
|
|
57
|
+
await executor?.close(); await runtime?.close?.();
|
|
58
|
+
const remaining = status();
|
|
59
|
+
if (remaining.views || remaining.inflight || remaining.executor || remaining.uncertain) throw Error('generation_cleanup_unconfirmed');
|
|
60
|
+
send(input, 'closed', remaining);
|
|
61
|
+
channel.close(); // Natural exit, no kill, after all owned work is closed.
|
|
62
|
+
} else {
|
|
63
|
+
if (!runtime || closing) throw Error('generation_unavailable');
|
|
64
|
+
const { name } = input.payload;
|
|
65
|
+
let args = input.payload.arguments;
|
|
66
|
+
const tool = runtime.tool?.(name) ?? runtime.tools.find(t => t.name === name);
|
|
67
|
+
if (!tool || tool.requiresUi && input.context.uiSupported !== true) throw Error('resource_denied');
|
|
68
|
+
++inflight;
|
|
69
|
+
const parentOwned = typeof tool.parentOwned === 'function' ? tool.parentOwned(args) : tool.parentOwned;
|
|
70
|
+
const started = performance.now();
|
|
71
|
+
let result, phase = parentOwned || !executor ? 'parse' : 'execute';
|
|
72
|
+
try {
|
|
73
|
+
phase = 'parse';
|
|
74
|
+
args = runtime.prepareInput?.(name, args) ?? args;
|
|
75
|
+
phase = parentOwned || !executor ? 'parse' : 'execute';
|
|
76
|
+
if (parentOwned || !executor) {
|
|
77
|
+
const parsed = tool.parse(args); phase = 'execute';
|
|
78
|
+
result = await tool.execute(parsed, undefined, input.context);
|
|
79
|
+
} else result = await executor.execute(args, { toolName: name, trusted: runtime.prepareDispatch?.(name, args) });
|
|
80
|
+
if (runtime.postprocessToolResult) result = await runtime.postprocessToolResult(name, args, result, input.context);
|
|
81
|
+
if (input.original_operation_id && result.outcome === 'effect_unknown') unknown.add(input.original_operation_id);
|
|
82
|
+
if (name === 'agdf_inspect' && args.operation === 'operation-status' && result.report?.status === 'recorded'
|
|
83
|
+
&& (result.report.receipt?.operation_id ?? result.report.receipt?.operation?.operation_id) === args.module.split('/').at(-1)) unknown.delete(args.module.split('/').at(-1));
|
|
84
|
+
const projected = runtime.projectToolResult(name, args, result);
|
|
85
|
+
journal(name, args, projected);
|
|
86
|
+
send(input, 'result', projected);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const facts = runtime.normalizeFailure(error, { phase });
|
|
89
|
+
if (input.original_operation_id && facts.dispatch !== 'not_dispatched') unknown.add(input.original_operation_id);
|
|
90
|
+
const projected = runtime.projectToolResult(name, args, runtime.failure(facts.code, name, args, { ...facts, elapsed_ms: performance.now() - started }), facts);
|
|
91
|
+
journal(name, args, projected);
|
|
92
|
+
send(input, 'result', projected);
|
|
93
|
+
} finally { --inflight; }
|
|
94
|
+
}
|
|
95
|
+
} catch (error) { if (input.kind === 'qualify') starting = false; send(input, 'failure', { reason: generationFailureReason(error) }); }
|
|
96
|
+
});
|
|
97
|
+
channel.on('disconnect', () => {
|
|
98
|
+
// Loss of the facade does not cancel or replay business effects. Timers and
|
|
99
|
+
// existing worker/view owners retain their bounds; no process-wide force kill.
|
|
100
|
+
if (!closing) { void executor?.close().catch(() => {}); void runtime?.close?.().catch(() => {}); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
}
|
|
@@ -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() } });
|