@aiwg/cli 2026.8.0 → 2026.8.1
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 +33 -0
- package/agentic/code/providers/capability-matrix.yaml +511 -0
- package/agentic/code/providers/model-capabilities.v1.json +120 -0
- package/agentic/code/providers/model-catalog.v1.json +96 -0
- package/agentic/code/providers/model-policy-evaluations.v1.json +50 -0
- package/agentic/code/providers/premium-model-allowlist.v1.json +36 -0
- package/bin/aiwg.mjs +14 -10
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/cli.js +2 -0
- package/dist/src/artifacts/types.js +4 -0
- package/dist/src/auth/client.js +209 -0
- package/dist/src/auth/config.js +38 -0
- package/dist/src/auth/credential-store.js +141 -0
- package/dist/src/auth/resource-credentials.js +25 -0
- package/dist/src/auth/types.js +2 -0
- package/dist/src/channel/manager.mjs +5 -5
- package/dist/src/cli/handlers/auth.js +125 -0
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/resource-versions.js +2 -0
- package/dist/src/cli/handlers/sessions.js +23 -5
- package/dist/src/cli/handlers/subcommands.js +10 -1
- package/dist/src/cli/handlers/use.js +342 -43
- package/dist/src/config/gitignore.js +1 -0
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/memory/canonical-context.js +342 -0
- package/dist/src/memory/context-pack.js +282 -0
- package/dist/src/memory/index.js +4 -0
- package/dist/src/memory/intake.js +118 -0
- package/dist/src/resources/resolver.js +1 -0
- package/dist/src/resources/web-release.d.ts +3 -1
- package/dist/src/resources/web-release.js +14 -6
- package/dist/src/serve/agentic-sandbox-fleet-client.js +213 -0
- package/dist/src/serve/fleet-mission-conductor.js +293 -0
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/output-registration.js +338 -0
- package/dist/src/sessions/promotion.js +73 -2
- package/dist/src/sessions/repository.js +2 -1
- package/dist/src/update/notifier.mjs +13 -2
- package/package.json +8 -1
- package/tools/_resolve-impl.mjs +74 -0
- package/tools/agents/deploy-agents.mjs +962 -0
- package/tools/agents/providers/base.mjs +2954 -0
- package/tools/agents/providers/claude.mjs +711 -0
- package/tools/agents/providers/codex.mjs +699 -0
- package/tools/agents/providers/copilot.mjs +659 -0
- package/tools/agents/providers/cursor.mjs +714 -0
- package/tools/agents/providers/factory.mjs +1130 -0
- package/tools/agents/providers/hermes.mjs +663 -0
- package/tools/agents/providers/hook-capabilities.mjs +85 -0
- package/tools/agents/providers/model-role.mjs +56 -0
- package/tools/agents/providers/openclaw-translator.mjs +348 -0
- package/tools/agents/providers/openclaw.mjs +680 -0
- package/tools/agents/providers/opencode.mjs +675 -0
- package/tools/agents/providers/openhuman.mjs +292 -0
- package/tools/agents/providers/warp.mjs +413 -0
- package/tools/agents/providers/windsurf.mjs +748 -0
- package/tools/commands/deploy-prompts-codex.mjs +336 -0
- package/tools/plugin/package-plugins.mjs +1013 -0
- package/tools/skills/deploy-skills-codex.mjs +571 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet Mission Conductor — management-plane orchestration over N executor
|
|
3
|
+
* targets using the neutral fleet-workload/v1 vocabulary.
|
|
4
|
+
*
|
|
5
|
+
* AIWG owns parent fan-out, aggregation, audit, and completion. Execution
|
|
6
|
+
* substrates (Agentic Sandbox first, other adapters later) own target-local
|
|
7
|
+
* lifecycle and report revisioned observations through the RunFleetWorker seam.
|
|
8
|
+
*
|
|
9
|
+
* @implements #1991
|
|
10
|
+
*/
|
|
11
|
+
import { routeMission } from './agent-router.js';
|
|
12
|
+
import { StackAdapterRegistry } from './stack-adapters.js';
|
|
13
|
+
const terminalFailures = new Set(['failed', 'cancelled', 'timed-out']);
|
|
14
|
+
const reviewStates = new Set(['unknown', 'operator-review-required']);
|
|
15
|
+
function lifecycleSatisfied(kind, state, health) {
|
|
16
|
+
switch (kind) {
|
|
17
|
+
case 'one-shot-command': return state === 'succeeded';
|
|
18
|
+
case 'persistent-agent': return state === 'retained' || state === 'detached' || state === 'running';
|
|
19
|
+
case 'daemon': return (state === 'healthy' || state === 'running') && health === 'healthy';
|
|
20
|
+
case 'scheduled-collector': return state === 'scheduled' || state === 'succeeded';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function collapseEvents(events) {
|
|
24
|
+
let latest;
|
|
25
|
+
const stale = [];
|
|
26
|
+
const artifacts = new Map();
|
|
27
|
+
for (const event of events) {
|
|
28
|
+
if (latest && event.revision <= latest.revision) {
|
|
29
|
+
stale.push(event.revision);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
latest = event;
|
|
33
|
+
for (const artifact of event.artifacts ?? [])
|
|
34
|
+
artifacts.set(`${artifact.kind}:${artifact.uri}`, artifact);
|
|
35
|
+
}
|
|
36
|
+
return { latest, stale, artifacts: [...artifacts.values()] };
|
|
37
|
+
}
|
|
38
|
+
function emptyAggregation(policy) {
|
|
39
|
+
return {
|
|
40
|
+
policy,
|
|
41
|
+
successful: 0,
|
|
42
|
+
failed: 0,
|
|
43
|
+
pending: 0,
|
|
44
|
+
reviewRequired: 0,
|
|
45
|
+
evidenceMissing: [],
|
|
46
|
+
completionEligible: false,
|
|
47
|
+
reason: 'children have not been evaluated',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function aggregate(policy, cycles) {
|
|
51
|
+
const successful = cycles.filter((cycle) => cycle.satisfied).length;
|
|
52
|
+
const failed = cycles.filter((cycle) => terminalFailures.has(cycle.observedState)).length;
|
|
53
|
+
const reviewRequired = cycles.filter((cycle) => reviewStates.has(cycle.observedState)).length;
|
|
54
|
+
const pending = cycles.length - successful - failed - reviewRequired;
|
|
55
|
+
const evidenceMissing = cycles.filter((cycle) => !cycle.evidenceComplete).map((cycle) => cycle.cycleId);
|
|
56
|
+
const allEvidence = evidenceMissing.length === 0;
|
|
57
|
+
let policySatisfied = false;
|
|
58
|
+
let reason = '';
|
|
59
|
+
switch (policy.mode) {
|
|
60
|
+
case 'all-pass':
|
|
61
|
+
policySatisfied = cycles.length > 0 && successful === cycles.length;
|
|
62
|
+
reason = `${successful}/${cycles.length} children satisfied all-pass`;
|
|
63
|
+
break;
|
|
64
|
+
case 'quorum':
|
|
65
|
+
policySatisfied = successful >= policy.minimumSuccesses;
|
|
66
|
+
reason = `${successful}/${policy.minimumSuccesses} quorum successes`;
|
|
67
|
+
break;
|
|
68
|
+
case 'best-output':
|
|
69
|
+
policySatisfied = cycles.some((cycle) => cycle.satisfied && Boolean(cycle.output));
|
|
70
|
+
reason = policySatisfied ? 'at least one satisfied child produced output' : 'no satisfied child produced output';
|
|
71
|
+
break;
|
|
72
|
+
case 'fail-fast':
|
|
73
|
+
policySatisfied = failed === 0 && cycles.length > 0 && successful === cycles.length;
|
|
74
|
+
reason = failed > 0 ? 'a child reached a terminal failure' : `${successful}/${cycles.length} children satisfied`;
|
|
75
|
+
break;
|
|
76
|
+
case 'manual-review':
|
|
77
|
+
policySatisfied = false;
|
|
78
|
+
reason = 'manual-review policy requires an audited operator decision';
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (reviewRequired > 0)
|
|
82
|
+
reason = `${reviewRequired} child observation(s) require operator review`;
|
|
83
|
+
if (!allEvidence)
|
|
84
|
+
reason = `required evidence missing for: ${evidenceMissing.join(', ')}`;
|
|
85
|
+
return {
|
|
86
|
+
policy,
|
|
87
|
+
successful,
|
|
88
|
+
failed,
|
|
89
|
+
pending,
|
|
90
|
+
reviewRequired,
|
|
91
|
+
evidenceMissing,
|
|
92
|
+
completionEligible: policySatisfied && allEvidence && reviewRequired === 0,
|
|
93
|
+
reason,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function cloneLedger(ledger) {
|
|
97
|
+
return structuredClone(ledger);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Runs every newly admitted child concurrently. Target allocation happens
|
|
101
|
+
* before execution, so one slow child cannot serialize unrelated work.
|
|
102
|
+
*/
|
|
103
|
+
export class FleetMissionConductor {
|
|
104
|
+
adapters;
|
|
105
|
+
runWorker;
|
|
106
|
+
scoreOutput;
|
|
107
|
+
ledgerStore;
|
|
108
|
+
/** Prevent concurrent child completions from committing snapshots out of order. */
|
|
109
|
+
persistQueue = Promise.resolve();
|
|
110
|
+
constructor(options) {
|
|
111
|
+
this.adapters = options.adapters ?? new StackAdapterRegistry();
|
|
112
|
+
this.runWorker = options.runWorker;
|
|
113
|
+
this.scoreOutput = options.scoreOutput ?? ((result) => result.output?.length ?? 0);
|
|
114
|
+
this.ledgerStore = options.ledgerStore;
|
|
115
|
+
}
|
|
116
|
+
async conduct(plan, pool, resumeFrom) {
|
|
117
|
+
const priorById = new Map((resumeFrom?.cycles ?? []).map((cycle) => [cycle.cycleId, cycle]));
|
|
118
|
+
const ledger = {
|
|
119
|
+
missionId: plan.missionId,
|
|
120
|
+
goal: plan.goal,
|
|
121
|
+
completionCriterion: plan.completionCriterion,
|
|
122
|
+
activityLog: [`mission ${plan.missionId} fleet start — ${plan.cycles.length} child workload(s)`],
|
|
123
|
+
cycles: [],
|
|
124
|
+
totalCost: 0,
|
|
125
|
+
checkpoint: { completed: [], pending: plan.cycles.map((cycle) => cycle.id), failed: [] },
|
|
126
|
+
runtimesUsed: [],
|
|
127
|
+
parentState: 'running',
|
|
128
|
+
aggregation: emptyAggregation(plan.aggregation),
|
|
129
|
+
};
|
|
130
|
+
const admitted = [];
|
|
131
|
+
const usedExecutors = new Set();
|
|
132
|
+
for (const cycle of plan.cycles) {
|
|
133
|
+
const prior = priorById.get(cycle.id);
|
|
134
|
+
if (prior?.satisfied && prior.evidenceComplete && !reviewStates.has(prior.observedState)) {
|
|
135
|
+
ledger.cycles.push(prior);
|
|
136
|
+
ledger.activityLog.push(`child ${cycle.id} re-adopted from durable completed state`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const adapter = this.adapters.get(cycle.runtime);
|
|
140
|
+
if (!adapter) {
|
|
141
|
+
ledger.cycles.push(this.unroutable(cycle, `no stack adapter registered for runtime '${cycle.runtime}'`));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
let candidates = pool;
|
|
145
|
+
if (cycle.targetId)
|
|
146
|
+
candidates = candidates.filter((executor) => executor.executorId === cycle.targetId);
|
|
147
|
+
if (plan.requireDistinctTargets !== false) {
|
|
148
|
+
candidates = candidates.filter((executor) => !usedExecutors.has(executor.executorId));
|
|
149
|
+
}
|
|
150
|
+
const routing = routeMission(candidates, {
|
|
151
|
+
capabilities: [adapter.runtimeCapability, ...(cycle.requiredCapabilities ?? [])],
|
|
152
|
+
}, cycle.longRunning ?? false);
|
|
153
|
+
if (!routing.selected) {
|
|
154
|
+
ledger.cycles.push(this.unroutable(cycle, 'no distinct connected executor satisfies the child requirements'));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const executor = routing.selected.executor;
|
|
158
|
+
usedExecutors.add(executor.executorId);
|
|
159
|
+
const dispatchId = cycle.dispatchId ?? `${plan.missionId}:${cycle.id}:dispatch`;
|
|
160
|
+
const lineage = {
|
|
161
|
+
orchestratorId: plan.orchestratorId,
|
|
162
|
+
missionId: plan.missionId,
|
|
163
|
+
dispatchId,
|
|
164
|
+
idempotencyKey: cycle.idempotencyKey ?? dispatchId,
|
|
165
|
+
parentId: plan.missionId,
|
|
166
|
+
childId: cycle.id,
|
|
167
|
+
targetId: cycle.targetId ?? executor.executorId,
|
|
168
|
+
executorId: executor.executorId,
|
|
169
|
+
runtimeId: cycle.runtime,
|
|
170
|
+
};
|
|
171
|
+
admitted.push({ cycle, executor, invocation: adapter.invoke(cycle.prompt), lineage });
|
|
172
|
+
ledger.activityLog.push(`child ${cycle.id} admitted on target ${lineage.targetId} as ${cycle.workloadKind}`);
|
|
173
|
+
}
|
|
174
|
+
ledger.aggregation = aggregate(plan.aggregation, ledger.cycles);
|
|
175
|
+
await this.persist(ledger);
|
|
176
|
+
await Promise.all(admitted.map(async ({ cycle, executor, invocation, lineage }) => {
|
|
177
|
+
let result;
|
|
178
|
+
try {
|
|
179
|
+
const ran = await this.runWorker(cycle, executor, invocation, lineage);
|
|
180
|
+
const collapsed = collapseEvents(ran.events);
|
|
181
|
+
const latest = collapsed.latest;
|
|
182
|
+
const observedState = latest?.observedState ?? 'unknown';
|
|
183
|
+
const artifacts = collapsed.artifacts;
|
|
184
|
+
const required = cycle.requiredEvidence ?? [];
|
|
185
|
+
const evidenceComplete = required.every((kind) => artifacts.some((artifact) => artifact.kind === kind));
|
|
186
|
+
result = {
|
|
187
|
+
cycleId: cycle.id,
|
|
188
|
+
runtime: cycle.runtime,
|
|
189
|
+
primitive: invocation.primitive,
|
|
190
|
+
executorId: executor.executorId,
|
|
191
|
+
routed: true,
|
|
192
|
+
reason: latest ? `observed revision ${latest.revision}: ${observedState}` : 'runtime returned no lifecycle event',
|
|
193
|
+
output: ran.output,
|
|
194
|
+
cost: ran.cost ?? 0,
|
|
195
|
+
workloadKind: cycle.workloadKind,
|
|
196
|
+
lineage,
|
|
197
|
+
commandId: ran.commandId ?? latest?.commandId,
|
|
198
|
+
taskId: latest?.taskId,
|
|
199
|
+
sessionId: ran.sessionId ?? latest?.sessionId,
|
|
200
|
+
observedState,
|
|
201
|
+
revision: latest?.revision ?? 0,
|
|
202
|
+
lastSeen: latest?.lastSeen,
|
|
203
|
+
health: latest?.health,
|
|
204
|
+
backpressure: latest?.backpressure,
|
|
205
|
+
artifacts,
|
|
206
|
+
staleEventRevisions: collapsed.stale,
|
|
207
|
+
evidenceComplete,
|
|
208
|
+
satisfied: evidenceComplete && lifecycleSatisfied(cycle.workloadKind, observedState, latest?.health),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
result = {
|
|
213
|
+
cycleId: cycle.id,
|
|
214
|
+
runtime: cycle.runtime,
|
|
215
|
+
primitive: invocation.primitive,
|
|
216
|
+
executorId: executor.executorId,
|
|
217
|
+
routed: true,
|
|
218
|
+
reason: `worker error: ${error instanceof Error ? error.message : String(error)}`,
|
|
219
|
+
cost: 0,
|
|
220
|
+
workloadKind: cycle.workloadKind,
|
|
221
|
+
lineage,
|
|
222
|
+
observedState: 'unknown',
|
|
223
|
+
revision: 0,
|
|
224
|
+
artifacts: [],
|
|
225
|
+
staleEventRevisions: [],
|
|
226
|
+
evidenceComplete: (cycle.requiredEvidence ?? []).length === 0,
|
|
227
|
+
satisfied: false,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
ledger.cycles.push(result);
|
|
231
|
+
ledger.activityLog.push(`child ${cycle.id} observed ${result.observedState} at revision ${result.revision}`);
|
|
232
|
+
ledger.aggregation = aggregate(plan.aggregation, ledger.cycles);
|
|
233
|
+
await this.persist(ledger);
|
|
234
|
+
}));
|
|
235
|
+
// Stable plan order makes durable snapshots and UI projection deterministic.
|
|
236
|
+
const order = new Map(plan.cycles.map((cycle, index) => [cycle.id, index]));
|
|
237
|
+
ledger.cycles.sort((a, b) => (order.get(a.cycleId) ?? 0) - (order.get(b.cycleId) ?? 0));
|
|
238
|
+
ledger.totalCost = ledger.cycles.reduce((sum, cycle) => sum + cycle.cost, 0);
|
|
239
|
+
ledger.runtimesUsed = [...new Set(ledger.cycles.filter((cycle) => cycle.routed).map((cycle) => cycle.runtime))];
|
|
240
|
+
ledger.checkpoint = {
|
|
241
|
+
completed: ledger.cycles.filter((cycle) => cycle.satisfied).map((cycle) => cycle.cycleId),
|
|
242
|
+
failed: ledger.cycles.filter((cycle) => terminalFailures.has(cycle.observedState)).map((cycle) => cycle.cycleId),
|
|
243
|
+
pending: ledger.cycles.filter((cycle) => !cycle.satisfied && !terminalFailures.has(cycle.observedState)).map((cycle) => cycle.cycleId),
|
|
244
|
+
};
|
|
245
|
+
let best;
|
|
246
|
+
for (const cycle of ledger.cycles) {
|
|
247
|
+
if (!cycle.output || !cycle.satisfied)
|
|
248
|
+
continue;
|
|
249
|
+
const score = this.scoreOutput(cycle);
|
|
250
|
+
if (!best || score > best.score)
|
|
251
|
+
best = { cycleId: cycle.cycleId, runtime: cycle.runtime, output: cycle.output, score };
|
|
252
|
+
}
|
|
253
|
+
ledger.bestOutput = best;
|
|
254
|
+
ledger.aggregation = aggregate(plan.aggregation, ledger.cycles);
|
|
255
|
+
ledger.parentState = ledger.aggregation.completionEligible
|
|
256
|
+
? 'completed'
|
|
257
|
+
: ledger.aggregation.reviewRequired > 0 || plan.aggregation.mode === 'manual-review'
|
|
258
|
+
? 'operator-review-required'
|
|
259
|
+
: ledger.aggregation.failed > 0
|
|
260
|
+
? 'failed'
|
|
261
|
+
: 'pending';
|
|
262
|
+
ledger.activityLog.push(`mission ${plan.missionId} fleet end — ${ledger.parentState}: ${ledger.aggregation.reason}`);
|
|
263
|
+
await this.persist(ledger);
|
|
264
|
+
return ledger;
|
|
265
|
+
}
|
|
266
|
+
unroutable(cycle, reason) {
|
|
267
|
+
return {
|
|
268
|
+
cycleId: cycle.id,
|
|
269
|
+
runtime: cycle.runtime,
|
|
270
|
+
primitive: '(none)',
|
|
271
|
+
routed: false,
|
|
272
|
+
reason,
|
|
273
|
+
cost: 0,
|
|
274
|
+
workloadKind: cycle.workloadKind,
|
|
275
|
+
observedState: 'operator-review-required',
|
|
276
|
+
revision: 0,
|
|
277
|
+
artifacts: [],
|
|
278
|
+
staleEventRevisions: [],
|
|
279
|
+
evidenceComplete: (cycle.requiredEvidence ?? []).length === 0,
|
|
280
|
+
satisfied: false,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async persist(ledger) {
|
|
284
|
+
if (!this.ledgerStore)
|
|
285
|
+
return;
|
|
286
|
+
const snapshot = cloneLedger(ledger);
|
|
287
|
+
this.persistQueue = this.persistQueue.then(async () => {
|
|
288
|
+
await this.ledgerStore.save(snapshot);
|
|
289
|
+
});
|
|
290
|
+
await this.persistQueue;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=fleet-mission-conductor.js.map
|
|
@@ -9,6 +9,7 @@ export * from './optional-backends.js';
|
|
|
9
9
|
export * from './knowledge-shard.js';
|
|
10
10
|
export * from './candidates.js';
|
|
11
11
|
export * from './promotion.js';
|
|
12
|
+
export * from './output-registration.js';
|
|
12
13
|
export * from './importer.js';
|
|
13
14
|
export * from './import-lease.js';
|
|
14
15
|
export * from './batch-contracts.js';
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { SessionContractError, sha256 } from './contracts.js';
|
|
6
|
+
const DigestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
7
|
+
const ReferenceSchema = z.string().min(1).max(512).refine(value => !/[\r\n\0]/.test(value), 'references must be single-line inert locators');
|
|
8
|
+
export const OutputSourceReferenceSchema = z.object({
|
|
9
|
+
kind: z.enum(['file', 'url', 'note', 'session', 'artifact', 'context-pack']),
|
|
10
|
+
ref: ReferenceSchema,
|
|
11
|
+
digest: DigestSchema.nullable().default(null),
|
|
12
|
+
span: z.object({
|
|
13
|
+
start: z.number().int().nonnegative(),
|
|
14
|
+
end: z.number().int().positive(),
|
|
15
|
+
quoteDigest: DigestSchema,
|
|
16
|
+
}).strict().nullable().default(null),
|
|
17
|
+
}).strict().superRefine((value, context) => {
|
|
18
|
+
if (value.span && value.span.end <= value.span.start) {
|
|
19
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: 'source span end must exceed start' });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
export const OutputRegistrationRequestSchema = z.object({
|
|
23
|
+
outputPath: z.string().min(1),
|
|
24
|
+
mediaType: z.string().min(1).max(128),
|
|
25
|
+
contextPack: z.object({
|
|
26
|
+
id: ReferenceSchema,
|
|
27
|
+
digest: DigestSchema,
|
|
28
|
+
sources: z.array(OutputSourceReferenceSchema).min(1).max(256),
|
|
29
|
+
}).strict(),
|
|
30
|
+
supersedes: z.array(ReferenceSchema).max(128).default([]),
|
|
31
|
+
conflictsWith: z.array(ReferenceSchema).max(128).default([]),
|
|
32
|
+
}).strict();
|
|
33
|
+
/**
|
|
34
|
+
* Minimal incremental index sink. Each registration is independently atomic,
|
|
35
|
+
* discoverable, and replay-safe; corpus-wide index builders can consume these
|
|
36
|
+
* bounded records without rescanning output bodies.
|
|
37
|
+
*/
|
|
38
|
+
export class FilesystemDerivedOutputIndex {
|
|
39
|
+
root;
|
|
40
|
+
constructor(projectRoot) {
|
|
41
|
+
this.root = resolve(projectRoot, '.aiwg/memory/output-registration/index');
|
|
42
|
+
assertStorageRootInsideProject(projectRoot, this.root);
|
|
43
|
+
}
|
|
44
|
+
register(registration) {
|
|
45
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(registration.registrationId)) {
|
|
46
|
+
throw new Error('invalid output registration identity');
|
|
47
|
+
}
|
|
48
|
+
const filePath = resolve(this.root, `${registration.registrationId.replace(':', '_')}.json`);
|
|
49
|
+
const existing = readJsonIfPresent(filePath);
|
|
50
|
+
if (existing) {
|
|
51
|
+
if (JSON.stringify(existing) !== JSON.stringify(registration)) {
|
|
52
|
+
throw new SessionContractError('IMPORT_CONFLICT', 'derived output index identity already has different content');
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
writeJsonAtomic(filePath, registration);
|
|
57
|
+
}
|
|
58
|
+
registrations() {
|
|
59
|
+
if (!existsSync(this.root))
|
|
60
|
+
return [];
|
|
61
|
+
return readdirSync(this.root)
|
|
62
|
+
.filter(name => /^sha256_[0-9a-f]{64}\.json$/.test(name))
|
|
63
|
+
.sort()
|
|
64
|
+
.map(name => JSON.parse(readFileSync(resolve(this.root, name), 'utf8')));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function canonicalReference(value) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = new URL(value);
|
|
70
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
71
|
+
parsed.username = '';
|
|
72
|
+
parsed.password = '';
|
|
73
|
+
parsed.search = '';
|
|
74
|
+
parsed.hash = '';
|
|
75
|
+
return parsed.toString();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Non-URL references are opaque inert identifiers.
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function assertStorageRootInsideProject(projectRoot, storageRoot) {
|
|
84
|
+
const root = realpathSync(projectRoot);
|
|
85
|
+
const candidate = resolve(projectRoot, storageRoot);
|
|
86
|
+
if (candidate !== resolve(projectRoot) && !candidate.startsWith(`${resolve(projectRoot)}${sep}`)) {
|
|
87
|
+
throw new SessionContractError('SOURCE_OUTSIDE_ALLOWED_ROOT', 'memory storage must be inside the project');
|
|
88
|
+
}
|
|
89
|
+
let ancestor = candidate;
|
|
90
|
+
while (!existsSync(ancestor)) {
|
|
91
|
+
const parent = dirname(ancestor);
|
|
92
|
+
if (parent === ancestor)
|
|
93
|
+
break;
|
|
94
|
+
ancestor = parent;
|
|
95
|
+
}
|
|
96
|
+
const actualAncestor = realpathSync(ancestor);
|
|
97
|
+
if (actualAncestor !== root && !actualAncestor.startsWith(`${root}${sep}`)) {
|
|
98
|
+
throw new SessionContractError('SOURCE_OUTSIDE_ALLOWED_ROOT', 'memory storage cannot traverse a link outside the project');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function assertNoSecretMaterial(value, field) {
|
|
102
|
+
const secretAssignment = /(?:^|[?&;:\s])(?:api[_-]?key|access[_-]?token|token|secret|password|passwd|authorization)\s*[:=]\s*[^\s&;]+/i;
|
|
103
|
+
const privateKey = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/;
|
|
104
|
+
const providerToken = /(?:^|[^a-z0-9])(?:ghp|github_pat|sk|xox[baprs])_[a-z0-9_-]{16,}/i;
|
|
105
|
+
if (secretAssignment.test(value) || privateKey.test(value) || providerToken.test(value)) {
|
|
106
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', `${field} appears to contain secret material; store only a non-secret locator`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function canonicalRequest(request) {
|
|
110
|
+
const canonical = {
|
|
111
|
+
...request,
|
|
112
|
+
contextPack: {
|
|
113
|
+
...request.contextPack,
|
|
114
|
+
id: canonicalReference(request.contextPack.id),
|
|
115
|
+
sources: request.contextPack.sources.map(source => ({
|
|
116
|
+
...source,
|
|
117
|
+
ref: canonicalReference(source.ref),
|
|
118
|
+
})),
|
|
119
|
+
},
|
|
120
|
+
supersedes: [...new Set(request.supersedes.map(canonicalReference))].sort(),
|
|
121
|
+
conflictsWith: [...new Set(request.conflictsWith.map(canonicalReference))].sort(),
|
|
122
|
+
};
|
|
123
|
+
assertNoSecretMaterial(canonical.contextPack.id, 'context-pack identity');
|
|
124
|
+
for (const source of canonical.contextPack.sources) {
|
|
125
|
+
assertNoSecretMaterial(source.ref, 'source reference');
|
|
126
|
+
}
|
|
127
|
+
for (const value of [...canonical.supersedes, ...canonical.conflictsWith]) {
|
|
128
|
+
assertNoSecretMaterial(value, 'lifecycle reference');
|
|
129
|
+
}
|
|
130
|
+
return canonical;
|
|
131
|
+
}
|
|
132
|
+
function resolveImmutableOutput(projectRoot, requestedPath) {
|
|
133
|
+
if (requestedPath.includes('\0')) {
|
|
134
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'output path contains a null byte');
|
|
135
|
+
}
|
|
136
|
+
const pathSegments = requestedPath.toLocaleLowerCase().split(/[\\/]+/);
|
|
137
|
+
if (pathSegments.some(segment => segment === '.env'
|
|
138
|
+
|| segment === '.ssh'
|
|
139
|
+
|| /^(?:credentials?|secrets?|tokens?)(?:\.|$)/.test(segment))) {
|
|
140
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'sensitive credential/secret paths cannot be registered as ordinary outputs');
|
|
141
|
+
}
|
|
142
|
+
const root = realpathSync(projectRoot);
|
|
143
|
+
const candidate = realpathSync(resolve(root, requestedPath));
|
|
144
|
+
if (candidate !== root && !candidate.startsWith(`${root}${sep}`)) {
|
|
145
|
+
throw new SessionContractError('SOURCE_OUTSIDE_ALLOWED_ROOT', 'registered output must resolve inside the project');
|
|
146
|
+
}
|
|
147
|
+
return { absolute: candidate, locator: relative(root, candidate).split(sep).join('/') };
|
|
148
|
+
}
|
|
149
|
+
function registrationFor(projectRoot, raw) {
|
|
150
|
+
const request = canonicalRequest(OutputRegistrationRequestSchema.parse(raw));
|
|
151
|
+
const output = resolveImmutableOutput(projectRoot, request.outputPath);
|
|
152
|
+
const content = readFileSync(output.absolute);
|
|
153
|
+
const digest = `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
154
|
+
const identity = {
|
|
155
|
+
output: { locator: output.locator, mediaType: request.mediaType, digest, byteLength: content.length },
|
|
156
|
+
contextPack: request.contextPack,
|
|
157
|
+
supersedes: request.supersedes,
|
|
158
|
+
conflictsWith: request.conflictsWith,
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
schemaVersion: 'aiwg.output-registration.v1',
|
|
162
|
+
registrationId: sha256(JSON.stringify(identity)),
|
|
163
|
+
...identity,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export class FilesystemOutputRegistrationStore {
|
|
167
|
+
root;
|
|
168
|
+
outboxRoot;
|
|
169
|
+
receiptRoot;
|
|
170
|
+
constructor(projectRoot) {
|
|
171
|
+
this.root = resolve(projectRoot, '.aiwg/memory/output-registration');
|
|
172
|
+
assertStorageRootInsideProject(projectRoot, this.root);
|
|
173
|
+
this.outboxRoot = resolve(this.root, 'outbox');
|
|
174
|
+
this.receiptRoot = resolve(this.root, 'receipts');
|
|
175
|
+
}
|
|
176
|
+
getReceipt(registrationId) {
|
|
177
|
+
return readJsonIfPresent(this.receiptPath(registrationId));
|
|
178
|
+
}
|
|
179
|
+
begin(operationId, registration) {
|
|
180
|
+
const existing = readJsonIfPresent(this.outboxPath(registration.registrationId));
|
|
181
|
+
if (existing)
|
|
182
|
+
return existing;
|
|
183
|
+
const record = {
|
|
184
|
+
schemaVersion: 'aiwg.output-registration-outbox.v1',
|
|
185
|
+
operationId,
|
|
186
|
+
registration,
|
|
187
|
+
state: 'pending',
|
|
188
|
+
attempts: 0,
|
|
189
|
+
lastError: null,
|
|
190
|
+
updatedAt: new Date().toISOString(),
|
|
191
|
+
};
|
|
192
|
+
writeJsonAtomic(this.outboxPath(registration.registrationId), record);
|
|
193
|
+
return record;
|
|
194
|
+
}
|
|
195
|
+
fail(registrationId, message) {
|
|
196
|
+
const path = this.outboxPath(registrationId);
|
|
197
|
+
const record = readJsonIfPresent(path);
|
|
198
|
+
if (!record)
|
|
199
|
+
throw new Error(`missing output-registration outbox record: ${registrationId}`);
|
|
200
|
+
const failed = {
|
|
201
|
+
...record,
|
|
202
|
+
attempts: record.attempts + 1,
|
|
203
|
+
lastError: message.slice(0, 512),
|
|
204
|
+
updatedAt: new Date().toISOString(),
|
|
205
|
+
};
|
|
206
|
+
writeJsonAtomic(path, failed);
|
|
207
|
+
return failed;
|
|
208
|
+
}
|
|
209
|
+
complete(operationId, registration) {
|
|
210
|
+
const existing = this.getReceipt(registration.registrationId);
|
|
211
|
+
if (existing) {
|
|
212
|
+
unlinkIfPresent(this.outboxPath(registration.registrationId));
|
|
213
|
+
return { ...existing, duplicate: true };
|
|
214
|
+
}
|
|
215
|
+
const receipt = {
|
|
216
|
+
schemaVersion: 'aiwg.output-registration-receipt.v1',
|
|
217
|
+
receiptId: sha256(`${operationId}\0${registration.registrationId}`),
|
|
218
|
+
registrationId: registration.registrationId,
|
|
219
|
+
operationId,
|
|
220
|
+
outputLocator: registration.output.locator,
|
|
221
|
+
outputDigest: registration.output.digest,
|
|
222
|
+
contextPackId: registration.contextPack.id,
|
|
223
|
+
contextPackDigest: registration.contextPack.digest,
|
|
224
|
+
sourceRefs: registration.contextPack.sources.map(source => source.ref),
|
|
225
|
+
registeredAt: new Date().toISOString(),
|
|
226
|
+
duplicate: false,
|
|
227
|
+
};
|
|
228
|
+
writeJsonAtomic(this.receiptPath(registration.registrationId), receipt);
|
|
229
|
+
unlinkIfPresent(this.outboxPath(registration.registrationId));
|
|
230
|
+
return receipt;
|
|
231
|
+
}
|
|
232
|
+
pending() {
|
|
233
|
+
if (!existsSync(this.outboxRoot))
|
|
234
|
+
return [];
|
|
235
|
+
return readdirSync(this.outboxRoot)
|
|
236
|
+
.filter(name => /^sha256_[0-9a-f]{64}\.json$/.test(name))
|
|
237
|
+
.sort()
|
|
238
|
+
.map(name => JSON.parse(readFileSync(resolve(this.outboxRoot, name), 'utf8')));
|
|
239
|
+
}
|
|
240
|
+
safeName(registrationId) {
|
|
241
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(registrationId)) {
|
|
242
|
+
throw new Error('invalid output registration identity');
|
|
243
|
+
}
|
|
244
|
+
return `${registrationId.replace(':', '_')}.json`;
|
|
245
|
+
}
|
|
246
|
+
outboxPath(registrationId) {
|
|
247
|
+
return resolve(this.outboxRoot, this.safeName(registrationId));
|
|
248
|
+
}
|
|
249
|
+
receiptPath(registrationId) {
|
|
250
|
+
return resolve(this.receiptRoot, this.safeName(registrationId));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export class OutputRegistrationCoordinator {
|
|
254
|
+
store;
|
|
255
|
+
index;
|
|
256
|
+
projectRoot;
|
|
257
|
+
constructor(projectRoot, store, index) {
|
|
258
|
+
this.store = store;
|
|
259
|
+
this.index = index;
|
|
260
|
+
this.projectRoot = resolve(projectRoot);
|
|
261
|
+
}
|
|
262
|
+
preview(request) {
|
|
263
|
+
const registration = registrationFor(this.projectRoot, request);
|
|
264
|
+
const operationId = sha256(JSON.stringify({
|
|
265
|
+
registrationId: registration.registrationId,
|
|
266
|
+
outputDigest: registration.output.digest,
|
|
267
|
+
contextPackDigest: registration.contextPack.digest,
|
|
268
|
+
}));
|
|
269
|
+
return {
|
|
270
|
+
...registration,
|
|
271
|
+
operationId,
|
|
272
|
+
duplicate: Boolean(this.store.getReceipt(registration.registrationId)),
|
|
273
|
+
confirmationRequired: true,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async register(input) {
|
|
277
|
+
const preview = this.preview(input.request);
|
|
278
|
+
if (preview.operationId !== input.operationId) {
|
|
279
|
+
throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'output registration requires confirmation of the exact current preview');
|
|
280
|
+
}
|
|
281
|
+
const existing = this.store.getReceipt(preview.registrationId);
|
|
282
|
+
if (existing)
|
|
283
|
+
return { ...existing, duplicate: true };
|
|
284
|
+
const registration = registrationFor(this.projectRoot, input.request);
|
|
285
|
+
if (registration.registrationId !== preview.registrationId
|
|
286
|
+
|| registration.output.digest !== preview.output.digest) {
|
|
287
|
+
throw new SessionContractError('IMPORT_CONFLICT', 'output changed after registration preview');
|
|
288
|
+
}
|
|
289
|
+
this.store.begin(input.operationId, registration);
|
|
290
|
+
try {
|
|
291
|
+
await this.index.register(registration);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
this.store.fail(registration.registrationId, error instanceof Error ? error.message : String(error));
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
return this.store.complete(input.operationId, registration);
|
|
298
|
+
}
|
|
299
|
+
async replayPending() {
|
|
300
|
+
const receipts = [];
|
|
301
|
+
for (const record of this.store.pending()) {
|
|
302
|
+
try {
|
|
303
|
+
await this.index.register(record.registration);
|
|
304
|
+
receipts.push(this.store.complete(record.operationId, record.registration));
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
this.store.fail(record.registration.registrationId, error instanceof Error ? error.message : String(error));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return receipts;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function readJsonIfPresent(filePath) {
|
|
314
|
+
try {
|
|
315
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
if (error.code === 'ENOENT')
|
|
319
|
+
return null;
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function writeJsonAtomic(filePath, value) {
|
|
324
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
325
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
326
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
327
|
+
renameSync(temporary, filePath);
|
|
328
|
+
}
|
|
329
|
+
function unlinkIfPresent(filePath) {
|
|
330
|
+
try {
|
|
331
|
+
unlinkSync(filePath);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (error.code !== 'ENOENT')
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=output-registration.js.map
|