@fabric-harness/cli 0.1.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/LICENSE +202 -0
- package/README.md +53 -0
- package/dist/bin/fabric-harness.d.ts +3 -0
- package/dist/bin/fabric-harness.d.ts.map +1 -0
- package/dist/bin/fabric-harness.js +1497 -0
- package/dist/bin/fabric-harness.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,1497 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { promises as fs, watch } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { parseEnv } from 'node:util';
|
|
7
|
+
import { applyEnvModelProvider, buildWorkspace, compactSessionInStore, createConfiguredSessionStore, findWorkspaceRoot, inspectReplayFromStore, inspectSessionFromStore, describeAgentFile, getRequiredAgentDefinition, listAgentSummaries, listBuilds, listSessionSummariesFromStore, listApprovalStatesFromStore, listApprovalsFromStore, listCheckpointsFromStore, listTasksFromStore, loadAgentModule, loadFabricHarnessConfig, resolveAgentPath, resolveApprovalInStore, resolveSessionApproval, runAgent, startDevServer, cancelTaskInStore, getMetricsFromStore, getTaskFromStore, verifyAttestation, verifyProvenance, } from '@fabric-harness/node';
|
|
8
|
+
import { FileSessionStore } from '@fabric-harness/node';
|
|
9
|
+
import { EmptySandboxEnv, SchemaValidationError, createBuiltinTools, resolveModelProvider, toolsToModelSchemas } from '@fabric-harness/sdk';
|
|
10
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
11
|
+
import { createLocalTemporalActivities, createTemporalClient, startTemporalWorker } from '@fabric-harness/temporal';
|
|
12
|
+
const HELP = `Fabric Harness
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
fabric-harness --help
|
|
16
|
+
fabric-harness run <agent> [--target node|temporal-worker] [--model provider/model] [--id <id>] [--env <file>] [--payload '<json>' | --payload-file <path>]
|
|
17
|
+
fabric-harness run <agent> [--target node|temporal-worker] [--env <file>] --question "What is Temporal?"
|
|
18
|
+
fabric-harness run <agent> [--target node|temporal-worker] question="What is Temporal?"
|
|
19
|
+
fabric-harness run <agent> [--target temporal-worker] [--model provider/model] [--id <id>] --prompt <text>
|
|
20
|
+
fabric-harness agents [--json]
|
|
21
|
+
fabric-harness describe <agent> [--json]
|
|
22
|
+
fabric-harness build [--target node|temporal-worker|docker|foundry-hosted-agent|cloudflare] [--out <dir>] [--sbom] [--sbom-required] [--provenance] [--attestation] [--sign-provenance] [--signing-key <path|env://VAR>] [--docker-build] [--docker-push] [--docker-tag <tag>] [--image-sbom] [--image-sbom-required]
|
|
23
|
+
fabric-harness sessions
|
|
24
|
+
fabric-harness builds
|
|
25
|
+
fabric-harness verify-attestation <build-dir-or-attestation>
|
|
26
|
+
fabric-harness verify-provenance <build-dir-or-provenance>
|
|
27
|
+
fabric-harness doctor [--target node|temporal-worker] [--model provider/model] [--tools] [--live] [--json]
|
|
28
|
+
fabric-harness add [connector] [--print]
|
|
29
|
+
fabric-harness inspect <session-id>
|
|
30
|
+
fabric-harness logs <session-id> [--events]
|
|
31
|
+
fabric-harness checkpoints <session-id>
|
|
32
|
+
fabric-harness artifacts <session-id> [--json]
|
|
33
|
+
fabric-harness artifact get <session-id> <artifact-id-or-name> [--out <path>]
|
|
34
|
+
fabric-harness metrics <session-id> [--json]
|
|
35
|
+
fabric-harness tasks <session-id> [--json]
|
|
36
|
+
fabric-harness task <session-id> <task-id> [--json]
|
|
37
|
+
fabric-harness cancel-task <session-id> <task-id> [--actor <id>] [--reason <text>]
|
|
38
|
+
fabric-harness compact <session-id> [--keep <n>] [--summary <text>]
|
|
39
|
+
fabric-harness replay <session-id>
|
|
40
|
+
fabric-harness approvals <session-id> [--pending] [--state]
|
|
41
|
+
fabric-harness approve <session-id> <approval-id> [--actor <id>] [--reason <text>]
|
|
42
|
+
fabric-harness reject <session-id> <approval-id> [--actor <id>] [--reason <text>]
|
|
43
|
+
fabric-harness dev [--target node|cloudflare] [--env <file>] [--host <host>] [--port <port>] [--auth-token-env <var>] [--max-body-bytes <n>] [--rate-limit-window-ms <n>] [--rate-limit-max <n>]
|
|
44
|
+
fabric-harness temporal-worker [--task-queue <name>] [--address <host:port>] [--env <file>]
|
|
45
|
+
|
|
46
|
+
Commands:
|
|
47
|
+
run <agent> Run a local .fabricharness agent handler
|
|
48
|
+
build Compile workspace and emit a deployment manifest
|
|
49
|
+
agents List workspace agents
|
|
50
|
+
describe <agent> Show agent metadata, schemas, and examples
|
|
51
|
+
sessions List persisted sessions
|
|
52
|
+
builds List emitted build manifests
|
|
53
|
+
inspect <session-id> Inspect persisted session history
|
|
54
|
+
logs <session-id> Print a readable session timeline
|
|
55
|
+
checkpoints <id> List checkpoints in a persisted session
|
|
56
|
+
artifacts <id> List artifacts in a persisted session
|
|
57
|
+
artifact get <id> Download/print a session artifact
|
|
58
|
+
metrics <id> Summarize token/tool/shell/artifact metrics
|
|
59
|
+
tasks <id> List durable tasks in a persisted session
|
|
60
|
+
task <id> <task-id> Show durable task status and checkpoints
|
|
61
|
+
cancel-task <id> Append a cancellation marker for a running task
|
|
62
|
+
compact <id> Add a compaction entry to a persisted session
|
|
63
|
+
replay <id> Show read-only active path and model context
|
|
64
|
+
approvals <id> List approval requests for a persisted session
|
|
65
|
+
approve <id> <app> Mark an approval request approved
|
|
66
|
+
reject <id> <app> Mark an approval request denied
|
|
67
|
+
dev Start local HTTP/SSE dev server
|
|
68
|
+
temporal-worker Start a local Temporal worker with Fabric activities
|
|
69
|
+
add List or print connector implementation recipes
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
--id <id> Run or agent identifier. If omitted, a session id is generated.
|
|
73
|
+
--payload <json> JSON payload passed to the agent
|
|
74
|
+
--payload-file <p> Read JSON payload from a file
|
|
75
|
+
--stdin Read JSON payload from standard input
|
|
76
|
+
--set k=v Set a payload field. May be repeated
|
|
77
|
+
--<field> <value> Set a payload field shortcut, e.g. --question "..."
|
|
78
|
+
--runtime <mode> inline or temporal for run
|
|
79
|
+
--target <target> Run/build target. For run: node or temporal-worker
|
|
80
|
+
--prompt <text> Prompt text for temporal run mode
|
|
81
|
+
--model <model> Model as provider/model-id, e.g. openai/gpt-5.5
|
|
82
|
+
--env <file> Load .env-style variables before run/dev/temporal-worker. Auto-loads .env/.env.local; shell env wins.
|
|
83
|
+
|
|
84
|
+
Config defaults:
|
|
85
|
+
.fabricharness/config.ts may set run.target, run.model, run.idPrefix,
|
|
86
|
+
temporal.address, temporal.taskQueue, and agent.model. CLI flags win over config.
|
|
87
|
+
--target <target> Build target: node, temporal-worker, docker, foundry-hosted-agent, or cloudflare
|
|
88
|
+
--out <dir> Build output directory
|
|
89
|
+
--sbom Emit CycloneDX SBOM with Syft when available
|
|
90
|
+
--sbom-required Require SBOM generation and fail if Syft is missing
|
|
91
|
+
--provenance Emit provenance.json for the build artifact
|
|
92
|
+
--attestation Emit attestation.intoto.jsonl with manifest digest subject
|
|
93
|
+
--sign-provenance Sign provenance.json with cosign sign-blob
|
|
94
|
+
--signing-key <k> cosign key path or env://VAR, defaults to env://COSIGN_PRIVATE_KEY
|
|
95
|
+
--docker-build Run docker build after emitting --target docker
|
|
96
|
+
--docker-push Run docker push after --docker-build
|
|
97
|
+
--docker-tag <tag> Tag to use with --docker-build/--docker-push
|
|
98
|
+
--image-sbom Emit CycloneDX SBOM for the built Docker image with Syft
|
|
99
|
+
--image-sbom-required Require image SBOM generation and fail if Syft is missing
|
|
100
|
+
-h, --help Show help
|
|
101
|
+
`;
|
|
102
|
+
function parseJsonPayload(value) {
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(value);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
throw new Error(`Invalid --payload JSON: ${message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function parsePayloadValue(value) {
|
|
112
|
+
const trimmed = value.trim();
|
|
113
|
+
if (trimmed === 'true')
|
|
114
|
+
return true;
|
|
115
|
+
if (trimmed === 'false')
|
|
116
|
+
return false;
|
|
117
|
+
if (trimmed === 'null')
|
|
118
|
+
return null;
|
|
119
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed))
|
|
120
|
+
return Number(trimmed);
|
|
121
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']')))
|
|
122
|
+
return parseJsonPayload(trimmed);
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
function applyPayloadField(fields, assignment) {
|
|
126
|
+
const equals = assignment.indexOf('=');
|
|
127
|
+
if (equals <= 0)
|
|
128
|
+
throw new Error(`Expected payload assignment key=value, got: ${assignment}`);
|
|
129
|
+
fields[assignment.slice(0, equals)] = parsePayloadValue(assignment.slice(equals + 1));
|
|
130
|
+
}
|
|
131
|
+
function parseRunArgs(args) {
|
|
132
|
+
const [agent, ...rest] = args;
|
|
133
|
+
if (!agent || agent.startsWith('-'))
|
|
134
|
+
throw new Error('Missing required <agent> argument for run.');
|
|
135
|
+
const parsed = { agent };
|
|
136
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
137
|
+
const arg = rest[index];
|
|
138
|
+
if (arg === undefined)
|
|
139
|
+
continue;
|
|
140
|
+
if (arg === '--id') {
|
|
141
|
+
const value = rest[++index];
|
|
142
|
+
if (!value)
|
|
143
|
+
throw new Error('Missing value for --id.');
|
|
144
|
+
parsed.id = value;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (arg === '--payload') {
|
|
148
|
+
const value = rest[++index];
|
|
149
|
+
if (!value)
|
|
150
|
+
throw new Error('Missing value for --payload.');
|
|
151
|
+
parsed.payload = parseJsonPayload(value);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (arg === '--payload-file') {
|
|
155
|
+
const value = rest[++index];
|
|
156
|
+
if (!value)
|
|
157
|
+
throw new Error('Missing value for --payload-file.');
|
|
158
|
+
parsed.payloadFile = value;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (arg === '--stdin') {
|
|
162
|
+
parsed.stdin = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (arg === '--set') {
|
|
166
|
+
const value = rest[++index];
|
|
167
|
+
if (!value)
|
|
168
|
+
throw new Error('Missing value for --set.');
|
|
169
|
+
parsed.payloadFields ??= {};
|
|
170
|
+
applyPayloadField(parsed.payloadFields, value);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (arg === '--runtime') {
|
|
174
|
+
const value = rest[++index];
|
|
175
|
+
if (value !== 'inline' && value !== 'temporal')
|
|
176
|
+
throw new Error('--runtime must be inline or temporal.');
|
|
177
|
+
parsed.runtime = value;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (arg === '--target') {
|
|
181
|
+
const value = rest[++index];
|
|
182
|
+
if (value !== 'node' && value !== 'temporal-worker')
|
|
183
|
+
throw new Error('--target for run must be node or temporal-worker.');
|
|
184
|
+
parsed.target = value;
|
|
185
|
+
parsed.runtime = value === 'temporal-worker' ? 'temporal' : 'inline';
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (arg === '--prompt') {
|
|
189
|
+
const value = rest[++index];
|
|
190
|
+
if (!value)
|
|
191
|
+
throw new Error('Missing value for --prompt.');
|
|
192
|
+
parsed.prompt = value;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (arg === '--model') {
|
|
196
|
+
const value = rest[++index];
|
|
197
|
+
if (!value)
|
|
198
|
+
throw new Error('Missing value for --model.');
|
|
199
|
+
parsed.model = value;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (arg === '--env') {
|
|
203
|
+
const value = rest[++index];
|
|
204
|
+
if (!value)
|
|
205
|
+
throw new Error('Missing value for --env.');
|
|
206
|
+
parsed.envFiles ??= [];
|
|
207
|
+
parsed.envFiles.push(value);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (arg === '--help' || arg === '-h') {
|
|
211
|
+
console.log(HELP);
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
if (arg.startsWith('--')) {
|
|
215
|
+
const key = arg.slice(2);
|
|
216
|
+
if (!key)
|
|
217
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
218
|
+
const value = rest[++index];
|
|
219
|
+
if (value === undefined || value.startsWith('--'))
|
|
220
|
+
throw new Error(`Missing value for --${key}.`);
|
|
221
|
+
parsed.payloadFields ??= {};
|
|
222
|
+
parsed.payloadFields[key] = parsePayloadValue(value);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (arg.includes('=')) {
|
|
226
|
+
parsed.payloadFields ??= {};
|
|
227
|
+
applyPayloadField(parsed.payloadFields, arg);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
231
|
+
}
|
|
232
|
+
return parsed;
|
|
233
|
+
}
|
|
234
|
+
function printResult(result) {
|
|
235
|
+
if (typeof result === 'string') {
|
|
236
|
+
console.log(result);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (result !== undefined)
|
|
240
|
+
console.log(JSON.stringify(result, null, 2));
|
|
241
|
+
}
|
|
242
|
+
async function loadEnvFiles(envFiles, options = {}) {
|
|
243
|
+
if (!envFiles || envFiles.length === 0)
|
|
244
|
+
return;
|
|
245
|
+
const merged = {};
|
|
246
|
+
for (const envFile of envFiles) {
|
|
247
|
+
const filePath = path.resolve(process.cwd(), envFile);
|
|
248
|
+
let contents;
|
|
249
|
+
try {
|
|
250
|
+
contents = await fs.readFile(filePath, 'utf8');
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (options.ignoreMissing && error.code === 'ENOENT')
|
|
254
|
+
continue;
|
|
255
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
256
|
+
throw new Error(`Failed to read --env file ${envFile}: ${message}`);
|
|
257
|
+
}
|
|
258
|
+
Object.assign(merged, parseEnv(contents));
|
|
259
|
+
}
|
|
260
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
261
|
+
if (ORIGINAL_ENV[key] !== undefined)
|
|
262
|
+
continue;
|
|
263
|
+
if (options.explicit || process.env[key] === undefined)
|
|
264
|
+
process.env[key] = value;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function loadAutoEnvFiles() {
|
|
268
|
+
const cwd = process.cwd();
|
|
269
|
+
const workspaceRoot = await findWorkspaceRoot(cwd).catch(() => undefined);
|
|
270
|
+
const repoRoot = await findRepoRoot(cwd).catch(() => undefined);
|
|
271
|
+
const roots = [...new Set([repoRoot, workspaceRoot].filter((value) => Boolean(value)))];
|
|
272
|
+
const files = [];
|
|
273
|
+
for (const root of roots) {
|
|
274
|
+
files.push(path.join(root, '.env'), path.join(root, '.env.local'));
|
|
275
|
+
}
|
|
276
|
+
if (workspaceRoot) {
|
|
277
|
+
files.push(path.join(workspaceRoot, '.fabricharness', '.env'), path.join(workspaceRoot, '.fabricharness', '.env.local'));
|
|
278
|
+
}
|
|
279
|
+
await loadEnvFiles(files, { ignoreMissing: true });
|
|
280
|
+
}
|
|
281
|
+
async function findRepoRoot(start) {
|
|
282
|
+
let current = path.resolve(start);
|
|
283
|
+
for (;;) {
|
|
284
|
+
try {
|
|
285
|
+
await fs.access(path.join(current, 'pnpm-workspace.yaml'));
|
|
286
|
+
return current;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// keep walking
|
|
290
|
+
}
|
|
291
|
+
const parent = path.dirname(current);
|
|
292
|
+
if (parent === current)
|
|
293
|
+
return undefined;
|
|
294
|
+
current = parent;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async function runCommand(args) {
|
|
298
|
+
const parsedArgs = parseRunArgs(args);
|
|
299
|
+
await loadEnvFiles(parsedArgs.envFiles, { explicit: true });
|
|
300
|
+
const parsed = await resolvePayloadInputs(parsedArgs);
|
|
301
|
+
const resolved = await resolveRunDefaults(parsed);
|
|
302
|
+
if (resolved.runtime === 'temporal')
|
|
303
|
+
return runTemporalPromptCommand(resolved);
|
|
304
|
+
const { result } = await runAgent(resolved);
|
|
305
|
+
printResult(result);
|
|
306
|
+
}
|
|
307
|
+
async function resolvePayloadInputs(parsed) {
|
|
308
|
+
let payload = parsed.payload;
|
|
309
|
+
if (parsed.payloadFile) {
|
|
310
|
+
const filePath = path.resolve(process.cwd(), parsed.payloadFile);
|
|
311
|
+
payload = parseJsonPayload(await fs.readFile(filePath, 'utf8'));
|
|
312
|
+
}
|
|
313
|
+
if (parsed.stdin) {
|
|
314
|
+
payload = parseJsonPayload(await readStdin());
|
|
315
|
+
}
|
|
316
|
+
if (parsed.payloadFields && Object.keys(parsed.payloadFields).length > 0) {
|
|
317
|
+
const base = typeof payload === 'object' && payload !== null && !Array.isArray(payload) ? payload : {};
|
|
318
|
+
payload = { ...base, ...parsed.payloadFields };
|
|
319
|
+
}
|
|
320
|
+
return { ...parsed, ...(payload !== undefined ? { payload } : {}) };
|
|
321
|
+
}
|
|
322
|
+
async function readStdin() {
|
|
323
|
+
const chunks = [];
|
|
324
|
+
for await (const chunk of process.stdin)
|
|
325
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
326
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
327
|
+
}
|
|
328
|
+
async function resolveRunDefaults(parsed) {
|
|
329
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
330
|
+
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
331
|
+
const envTarget = process.env.FABRIC_TARGET ?? process.env.FABRIC_HARNESS_TARGET;
|
|
332
|
+
if (envTarget !== undefined && envTarget !== 'node' && envTarget !== 'temporal-worker')
|
|
333
|
+
throw new Error('FABRIC_TARGET/FABRIC_HARNESS_TARGET must be node or temporal-worker.');
|
|
334
|
+
const agentDefaults = await readAgentRunDefaults(workspaceRoot, parsed.agent);
|
|
335
|
+
const target = parsed.target ?? envTarget ?? config.run?.target ?? agentDefaults.target;
|
|
336
|
+
if (target !== undefined && target !== 'node' && target !== 'temporal-worker')
|
|
337
|
+
throw new Error('Configured run.target must be node or temporal-worker.');
|
|
338
|
+
const runtime = parsed.runtime ?? (target === 'temporal-worker' ? 'temporal' : 'inline');
|
|
339
|
+
const model = parsed.model ?? process.env.FABRIC_MODEL ?? config.run?.model ?? agentDefaults.model ?? (typeof config.agent?.model === 'string' ? config.agent.model : undefined);
|
|
340
|
+
const id = parsed.id ?? `${config.run?.idPrefix ?? parsed.agent}-${randomUUID()}`;
|
|
341
|
+
return {
|
|
342
|
+
...parsed,
|
|
343
|
+
id,
|
|
344
|
+
runtime,
|
|
345
|
+
...(target ? { target } : {}),
|
|
346
|
+
...(model ? { model } : {}),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function readAgentRunDefaults(workspaceRoot, agentName) {
|
|
350
|
+
try {
|
|
351
|
+
const agentPath = await resolveAgentPath(workspaceRoot, agentName);
|
|
352
|
+
const description = await describeAgentFile({ workspaceRoot, agentPath });
|
|
353
|
+
return {
|
|
354
|
+
...(description.model ? { model: description.model } : {}),
|
|
355
|
+
...(description.target ? { target: description.target } : {}),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return {};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function runTemporalPromptCommand(parsed) {
|
|
363
|
+
if (!parsed.id)
|
|
364
|
+
throw new Error('--id is required with --runtime temporal.');
|
|
365
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
366
|
+
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
367
|
+
const taskQueue = process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ?? 'fabric-harness';
|
|
368
|
+
const address = process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? 'localhost:7233';
|
|
369
|
+
const namespace = process.env.FABRIC_TEMPORAL_NAMESPACE ?? config.temporal?.namespace;
|
|
370
|
+
const client = await createTemporalClient({
|
|
371
|
+
mode: 'temporal',
|
|
372
|
+
address,
|
|
373
|
+
taskQueue,
|
|
374
|
+
...(namespace ? { namespace } : {}),
|
|
375
|
+
...(config.temporal?.apiKey ? { apiKey: config.temporal.apiKey } : {}),
|
|
376
|
+
...(config.temporal?.apiKeyEnv ? { apiKeyEnv: config.temporal.apiKeyEnv } : {}),
|
|
377
|
+
...(config.temporal?.tls ? { tls: config.temporal.tls } : {}),
|
|
378
|
+
workflowIdPrefix: config.temporal?.workflowIdPrefix ?? 'fabric-cli',
|
|
379
|
+
promptWorkflowMode: config.temporal?.promptWorkflowMode ?? 'hybrid',
|
|
380
|
+
});
|
|
381
|
+
try {
|
|
382
|
+
if (parsed.prompt) {
|
|
383
|
+
const runtime = client.createSessionRuntime(parsed.id);
|
|
384
|
+
printResult(await runtime.prompt({ text: parsed.prompt, ...(parsed.model ? { options: { model: parsed.model } } : {}) }));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const agentPath = await resolveAgentPath(workspaceRoot, parsed.agent);
|
|
388
|
+
const module = await loadAgentModule({ workspaceRoot, agentPath });
|
|
389
|
+
getRequiredAgentDefinition(module.default, agentPath);
|
|
390
|
+
const handler = module.default;
|
|
391
|
+
if (typeof handler !== 'function')
|
|
392
|
+
throw new Error(`Agent module must default-export agent({...}) from @fabric-harness/sdk: ${agentPath}`);
|
|
393
|
+
const context = createTemporalFabricContext(parsed, client.createSessionRuntime.bind(client));
|
|
394
|
+
printResult(await handler(context));
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
await client.close();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function createTemporalFabricContext(parsed, runtimeForSession) {
|
|
401
|
+
const payload = (typeof parsed.payload === 'object' && parsed.payload !== null && !Array.isArray(parsed.payload) ? parsed.payload : {});
|
|
402
|
+
return {
|
|
403
|
+
payload,
|
|
404
|
+
init: async (initOptions = {}) => {
|
|
405
|
+
const agentId = initOptions.id ?? parsed.id ?? 'temporal-agent';
|
|
406
|
+
const model = parsed.model ?? (typeof initOptions.model === 'string' ? initOptions.model : undefined);
|
|
407
|
+
return {
|
|
408
|
+
id: agentId,
|
|
409
|
+
...(model ? { model } : {}),
|
|
410
|
+
...(initOptions.role ? { role: initOptions.role } : {}),
|
|
411
|
+
session: async (id, _options) => createTemporalSession(id ?? parsed.id ?? agentId, runtimeForSession, model),
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function createTemporalSession(sessionId, runtimeForSession, model) {
|
|
417
|
+
const runtime = runtimeForSession(sessionId);
|
|
418
|
+
return {
|
|
419
|
+
id: sessionId,
|
|
420
|
+
agent: { id: sessionId, ...(model ? { model } : {}), session: async () => createTemporalSession(sessionId, runtimeForSession, model) },
|
|
421
|
+
// Do not create a rejected promise here: many agents never access session.sandbox,
|
|
422
|
+
// but Node treats an eagerly rejected promise as unhandled. Direct sandbox effects
|
|
423
|
+
// in Temporal CLI mode should go through session.shell()/tools, which are routed
|
|
424
|
+
// to workflow activities. This placeholder prevents accidental unhandled rejection.
|
|
425
|
+
sandbox: Promise.resolve(new EmptySandboxEnv()),
|
|
426
|
+
prompt: (text, options) => runtime.prompt({ text, ...(options || model ? { options: { ...(options ?? {}), ...(model && options?.model === undefined ? { model } : {}) } } : {}) }),
|
|
427
|
+
stream: async function* (text, options) { for (const event of [])
|
|
428
|
+
yield event; return await runtime.prompt({ text, ...(options || model ? { options: { ...(options ?? {}), ...(model && options?.model === undefined ? { model } : {}) } } : {}) }); },
|
|
429
|
+
skill: (name, options) => runtime.prompt({ text: `Run skill ${name}${options?.args ? ` with args ${JSON.stringify(options.args)}` : ''}`, ...(options || model ? { options: { ...(options ?? {}), ...(model && options?.model === undefined ? { model } : {}) } } : {}) }),
|
|
430
|
+
task: (text, options) => runtime.task({ text, ...(options?.id ? { taskId: options.id } : {}), ...(options || model ? { options: { ...(options ?? {}), ...(model && options?.model === undefined ? { model } : {}) } } : {}) }),
|
|
431
|
+
shell: (command, options) => runtime.shell({ command, ...(options ? { options } : {}) }),
|
|
432
|
+
history: async () => ({ id: sessionId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), entries: [], events: [] }),
|
|
433
|
+
checkpoint: {
|
|
434
|
+
create: (options) => runtime.checkpointCreate({ options }),
|
|
435
|
+
restore: (options) => runtime.checkpointRestore({ options }),
|
|
436
|
+
},
|
|
437
|
+
artifact: async () => { throw new Error('session.artifact() is not available in Temporal CLI shim yet. Use worker activities or a node target.'); },
|
|
438
|
+
compact: async () => ({ entryId: '', summary: 'Compaction is executed by Temporal worker activities.', compactedEntries: 0 }),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
async function temporalWorkerCommand(args) {
|
|
442
|
+
const envFiles = [];
|
|
443
|
+
let taskQueueFlag;
|
|
444
|
+
let addressFlag;
|
|
445
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
446
|
+
const arg = args[index];
|
|
447
|
+
if (arg === '--env') {
|
|
448
|
+
const value = args[++index];
|
|
449
|
+
if (!value)
|
|
450
|
+
throw new Error('Missing value for --env.');
|
|
451
|
+
envFiles.push(value);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (arg === '--task-queue') {
|
|
455
|
+
const value = args[++index];
|
|
456
|
+
if (!value)
|
|
457
|
+
throw new Error('Missing value for --task-queue.');
|
|
458
|
+
taskQueueFlag = value;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (arg === '--address') {
|
|
462
|
+
const value = args[++index];
|
|
463
|
+
if (!value)
|
|
464
|
+
throw new Error('Missing value for --address.');
|
|
465
|
+
addressFlag = value;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
469
|
+
}
|
|
470
|
+
await loadEnvFiles(envFiles, { explicit: true });
|
|
471
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
472
|
+
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
473
|
+
const taskQueue = taskQueueFlag ?? process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ?? 'fabric-harness';
|
|
474
|
+
const address = addressFlag ?? process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? 'localhost:7233';
|
|
475
|
+
const namespace = process.env.FABRIC_TEMPORAL_NAMESPACE ?? config.temporal?.namespace;
|
|
476
|
+
const sandbox = new EmptySandboxEnv();
|
|
477
|
+
const store = new FileSessionStore({ workspaceRoot });
|
|
478
|
+
const modelOptions = applyEnvModelProvider(config.agent ?? {});
|
|
479
|
+
const activities = createLocalTemporalActivities({
|
|
480
|
+
store,
|
|
481
|
+
sandbox,
|
|
482
|
+
tools: createBuiltinTools(sandbox),
|
|
483
|
+
...(typeof modelOptions.model === 'string' ? { model: modelOptions.model } : {}),
|
|
484
|
+
...(modelOptions.modelProvider ? { modelProvider: modelOptions.modelProvider } : {}),
|
|
485
|
+
...(modelOptions.policy ? { policy: modelOptions.policy } : {}),
|
|
486
|
+
...(modelOptions.resolveSecret ? { resolveSecret: modelOptions.resolveSecret } : {}),
|
|
487
|
+
});
|
|
488
|
+
const worker = await startTemporalWorker({
|
|
489
|
+
mode: 'temporal',
|
|
490
|
+
address,
|
|
491
|
+
taskQueue,
|
|
492
|
+
...(namespace ? { namespace } : {}),
|
|
493
|
+
...(config.temporal?.apiKey ? { apiKey: config.temporal.apiKey } : {}),
|
|
494
|
+
...(config.temporal?.apiKeyEnv ? { apiKeyEnv: config.temporal.apiKeyEnv } : {}),
|
|
495
|
+
...(config.temporal?.tls ? { tls: config.temporal.tls } : {}),
|
|
496
|
+
activities,
|
|
497
|
+
});
|
|
498
|
+
console.log(`Fabric Harness Temporal worker listening at ${address}, taskQueue=${taskQueue}`);
|
|
499
|
+
const shutdown = async () => {
|
|
500
|
+
await worker.close();
|
|
501
|
+
process.exit(0);
|
|
502
|
+
};
|
|
503
|
+
process.on('SIGINT', () => void shutdown());
|
|
504
|
+
process.on('SIGTERM', () => void shutdown());
|
|
505
|
+
await worker.runPromise;
|
|
506
|
+
}
|
|
507
|
+
async function buildCommand(args) {
|
|
508
|
+
let outDir;
|
|
509
|
+
let target;
|
|
510
|
+
let clean = true;
|
|
511
|
+
let sbom = false;
|
|
512
|
+
let sbomRequired = false;
|
|
513
|
+
let provenance = false;
|
|
514
|
+
let attestation = false;
|
|
515
|
+
let dockerBuild = false;
|
|
516
|
+
let dockerPush = false;
|
|
517
|
+
let dockerTag;
|
|
518
|
+
let imageSbom = false;
|
|
519
|
+
let imageSbomRequired = false;
|
|
520
|
+
let signProvenance = false;
|
|
521
|
+
let signingKey;
|
|
522
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
523
|
+
const arg = args[index];
|
|
524
|
+
if (arg === '--out') {
|
|
525
|
+
outDir = args[++index];
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (arg === '--target') {
|
|
529
|
+
const value = args[++index];
|
|
530
|
+
if (value !== 'node' && value !== 'temporal-worker' && value !== 'docker' && value !== 'foundry-hosted-agent' && value !== 'cloudflare')
|
|
531
|
+
throw new Error('--target must be node, temporal-worker, docker, foundry-hosted-agent, or cloudflare.');
|
|
532
|
+
target = value;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (arg === '--no-clean') {
|
|
536
|
+
clean = false;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (arg === '--sbom') {
|
|
540
|
+
sbom = true;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (arg === '--sbom-required') {
|
|
544
|
+
sbom = true;
|
|
545
|
+
sbomRequired = true;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (arg === '--provenance') {
|
|
549
|
+
provenance = true;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (arg === '--attestation') {
|
|
553
|
+
attestation = true;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (arg === '--sign-provenance') {
|
|
557
|
+
signProvenance = true;
|
|
558
|
+
provenance = true;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (arg === '--signing-key') {
|
|
562
|
+
signingKey = args[++index];
|
|
563
|
+
if (!signingKey)
|
|
564
|
+
throw new Error('Missing value for --signing-key.');
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (arg === '--docker-build') {
|
|
568
|
+
dockerBuild = true;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (arg === '--docker-push') {
|
|
572
|
+
dockerPush = true;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (arg === '--docker-tag') {
|
|
576
|
+
dockerTag = args[++index];
|
|
577
|
+
if (!dockerTag)
|
|
578
|
+
throw new Error('Missing value for --docker-tag.');
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (arg === '--image-sbom') {
|
|
582
|
+
imageSbom = true;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (arg === '--image-sbom-required') {
|
|
586
|
+
imageSbom = true;
|
|
587
|
+
imageSbomRequired = true;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
591
|
+
}
|
|
592
|
+
const result = await buildWorkspace({ ...(outDir ? { outDir } : {}), ...(target ? { target } : {}), clean, sbom, sbomRequired, provenance, attestation, signProvenance, ...(signingKey ? { signingKey } : {}), dockerBuild, dockerPush, ...(dockerTag ? { dockerTag } : {}), imageSbom, imageSbomRequired });
|
|
593
|
+
console.log(`Built Fabric Harness workspace: ${result.outDir}`);
|
|
594
|
+
console.log(`Manifest: ${result.manifestPath}`);
|
|
595
|
+
if (result.provenancePath)
|
|
596
|
+
console.log(`Provenance: ${result.provenancePath}`);
|
|
597
|
+
if (result.provenanceSignaturePath)
|
|
598
|
+
console.log(`Provenance signature: ${result.provenanceSignaturePath}`);
|
|
599
|
+
if (result.attestationPath)
|
|
600
|
+
console.log(`Attestation: ${result.attestationPath}`);
|
|
601
|
+
if (result.sbomPath)
|
|
602
|
+
console.log(`SBOM: ${result.sbomPath}`);
|
|
603
|
+
if (result.dockerImage)
|
|
604
|
+
console.log(`Docker image: ${result.dockerImage}`);
|
|
605
|
+
if (result.imageSbomPath)
|
|
606
|
+
console.log(`Image SBOM: ${result.imageSbomPath}`);
|
|
607
|
+
for (const warning of result.warnings)
|
|
608
|
+
console.warn(`Warning: ${warning}`);
|
|
609
|
+
console.log(`Agents: ${result.manifest.agents.map((agent) => agent.name).join(', ') || 'none'}`);
|
|
610
|
+
}
|
|
611
|
+
async function devCommand(args) {
|
|
612
|
+
let host;
|
|
613
|
+
let port;
|
|
614
|
+
let target = 'node';
|
|
615
|
+
let authTokenEnv;
|
|
616
|
+
let maxBodyBytes;
|
|
617
|
+
let rateLimitWindowMs;
|
|
618
|
+
let rateLimitMax;
|
|
619
|
+
const envFiles = [];
|
|
620
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
621
|
+
const arg = args[index];
|
|
622
|
+
if (arg === '--host') {
|
|
623
|
+
host = args[++index];
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (arg === '--port') {
|
|
627
|
+
const value = args[++index];
|
|
628
|
+
port = value ? Number(value) : undefined;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (arg === '--target') {
|
|
632
|
+
const value = args[++index];
|
|
633
|
+
if (value !== 'node' && value !== 'cloudflare')
|
|
634
|
+
throw new Error('--target for dev must be node or cloudflare.');
|
|
635
|
+
target = value;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (arg === '--env') {
|
|
639
|
+
const value = args[++index];
|
|
640
|
+
if (!value)
|
|
641
|
+
throw new Error('Missing value for --env.');
|
|
642
|
+
envFiles.push(value);
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (arg === '--auth-token-env') {
|
|
646
|
+
const value = args[++index];
|
|
647
|
+
if (!value)
|
|
648
|
+
throw new Error('Missing value for --auth-token-env.');
|
|
649
|
+
authTokenEnv = value;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (arg === '--max-body-bytes') {
|
|
653
|
+
const value = args[++index];
|
|
654
|
+
if (!value)
|
|
655
|
+
throw new Error('Missing value for --max-body-bytes.');
|
|
656
|
+
maxBodyBytes = Number(value);
|
|
657
|
+
if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0)
|
|
658
|
+
throw new Error('--max-body-bytes must be a positive number.');
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (arg === '--rate-limit-window-ms') {
|
|
662
|
+
const value = args[++index];
|
|
663
|
+
if (!value)
|
|
664
|
+
throw new Error('Missing value for --rate-limit-window-ms.');
|
|
665
|
+
rateLimitWindowMs = Number(value);
|
|
666
|
+
if (!Number.isFinite(rateLimitWindowMs) || rateLimitWindowMs <= 0)
|
|
667
|
+
throw new Error('--rate-limit-window-ms must be a positive number.');
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (arg === '--rate-limit-max') {
|
|
671
|
+
const value = args[++index];
|
|
672
|
+
if (!value)
|
|
673
|
+
throw new Error('Missing value for --rate-limit-max.');
|
|
674
|
+
rateLimitMax = Number(value);
|
|
675
|
+
if (!Number.isFinite(rateLimitMax) || rateLimitMax <= 0)
|
|
676
|
+
throw new Error('--rate-limit-max must be a positive number.');
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
680
|
+
}
|
|
681
|
+
await loadEnvFiles(envFiles, { explicit: true });
|
|
682
|
+
if (target === 'cloudflare') {
|
|
683
|
+
const cloudflareOptions = {};
|
|
684
|
+
if (host !== undefined)
|
|
685
|
+
cloudflareOptions.host = host;
|
|
686
|
+
if (port !== undefined)
|
|
687
|
+
cloudflareOptions.port = port;
|
|
688
|
+
await cloudflareDevCommand(cloudflareOptions);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const options = {};
|
|
692
|
+
if (host !== undefined)
|
|
693
|
+
options.host = host;
|
|
694
|
+
if (port !== undefined)
|
|
695
|
+
options.port = port;
|
|
696
|
+
if (authTokenEnv !== undefined)
|
|
697
|
+
options.authTokenEnv = authTokenEnv;
|
|
698
|
+
if (maxBodyBytes !== undefined)
|
|
699
|
+
options.maxBodyBytes = maxBodyBytes;
|
|
700
|
+
if (rateLimitWindowMs !== undefined || rateLimitMax !== undefined) {
|
|
701
|
+
options.rateLimit = { windowMs: rateLimitWindowMs ?? 60_000, maxRequests: rateLimitMax ?? 60 };
|
|
702
|
+
}
|
|
703
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
704
|
+
const server = await startDevServer({ ...options, cwd: workspaceRoot });
|
|
705
|
+
const watcher = startNodeDevWatcher(workspaceRoot, envFiles);
|
|
706
|
+
const shutdown = async () => {
|
|
707
|
+
watcher?.close();
|
|
708
|
+
await server.close();
|
|
709
|
+
process.exit(0);
|
|
710
|
+
};
|
|
711
|
+
process.on('SIGINT', () => void shutdown());
|
|
712
|
+
process.on('SIGTERM', () => void shutdown());
|
|
713
|
+
console.log(`Fabric Harness dev server listening at ${server.url}`);
|
|
714
|
+
console.log('POST /agents/:agent/:id, GET /sessions/:id, GET /sessions/:id/events');
|
|
715
|
+
console.log('Watching .fabricharness for changes; agents/config reload on the next request.');
|
|
716
|
+
}
|
|
717
|
+
async function cloudflareDevCommand(options) {
|
|
718
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
719
|
+
console.error('[fabric-harness] Building Cloudflare Worker artifact...');
|
|
720
|
+
const result = await buildWorkspace({ cwd: workspaceRoot, target: 'cloudflare' });
|
|
721
|
+
console.error(`[fabric-harness] Cloudflare artifact: ${result.outDir}`);
|
|
722
|
+
console.error('[fabric-harness] Starting Wrangler dev. Install wrangler/@cloudflare dependencies in the artifact if prompted.');
|
|
723
|
+
const args = ['wrangler', 'dev'];
|
|
724
|
+
if (options.port !== undefined)
|
|
725
|
+
args.push('--port', String(options.port));
|
|
726
|
+
if (options.host !== undefined)
|
|
727
|
+
args.push('--ip', options.host);
|
|
728
|
+
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
729
|
+
const child = spawn(npx, args, {
|
|
730
|
+
cwd: result.outDir,
|
|
731
|
+
stdio: 'inherit',
|
|
732
|
+
shell: false,
|
|
733
|
+
env: process.env,
|
|
734
|
+
});
|
|
735
|
+
const shutdown = () => {
|
|
736
|
+
child.kill('SIGTERM');
|
|
737
|
+
};
|
|
738
|
+
process.on('SIGINT', shutdown);
|
|
739
|
+
process.on('SIGTERM', shutdown);
|
|
740
|
+
const code = await new Promise((resolve, reject) => {
|
|
741
|
+
child.on('error', reject);
|
|
742
|
+
child.on('close', resolve);
|
|
743
|
+
});
|
|
744
|
+
if (code && code !== 0)
|
|
745
|
+
throw new Error(`Wrangler dev exited with code ${code}.`);
|
|
746
|
+
}
|
|
747
|
+
function startNodeDevWatcher(workspaceRoot, envFiles) {
|
|
748
|
+
const watchRoot = path.join(workspaceRoot, '.fabricharness');
|
|
749
|
+
try {
|
|
750
|
+
const envFileSet = new Set(envFiles.map((file) => path.resolve(process.cwd(), file)));
|
|
751
|
+
let timer;
|
|
752
|
+
return watch(watchRoot, { recursive: true }, (_eventType, filename) => {
|
|
753
|
+
if (timer)
|
|
754
|
+
clearTimeout(timer);
|
|
755
|
+
timer = setTimeout(() => {
|
|
756
|
+
const changed = filename ? path.join('.fabricharness', String(filename)) : '.fabricharness';
|
|
757
|
+
console.error(`[fabric-harness] Change detected: ${changed}. Reload will apply on next request.`);
|
|
758
|
+
for (const envFile of envFileSet) {
|
|
759
|
+
if (path.resolve(workspaceRoot, changed) === envFile)
|
|
760
|
+
console.error(`[fabric-harness] Env file changed: ${envFile}. Restart dev server to reload shell environment values.`);
|
|
761
|
+
}
|
|
762
|
+
}, 100);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
return undefined;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const CONNECTOR_RECIPES = {
|
|
770
|
+
daytona: 'sandbox--daytona.md',
|
|
771
|
+
e2b: 'sandbox--e2b.md',
|
|
772
|
+
modal: 'sandbox--modal.md',
|
|
773
|
+
'github-mcp': 'mcp--github.md',
|
|
774
|
+
postgres: 'data--postgres.md',
|
|
775
|
+
};
|
|
776
|
+
async function addCommand(args) {
|
|
777
|
+
let connector;
|
|
778
|
+
let print = false;
|
|
779
|
+
for (const arg of args) {
|
|
780
|
+
if (arg === '--print') {
|
|
781
|
+
print = true;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (!connector && !arg.startsWith('-')) {
|
|
785
|
+
connector = arg;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
throw new Error(`Unknown argument for add: ${arg}`);
|
|
789
|
+
}
|
|
790
|
+
if (!connector) {
|
|
791
|
+
console.log('Available connector recipes:');
|
|
792
|
+
for (const [name, file] of Object.entries(CONNECTOR_RECIPES))
|
|
793
|
+
console.log(` ${name.padEnd(12)} ${file}`);
|
|
794
|
+
console.log('\nUse `fabric-harness add <name> --print` to print a recipe.');
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const recipeFile = CONNECTOR_RECIPES[connector];
|
|
798
|
+
if (!recipeFile)
|
|
799
|
+
throw new Error(`Unknown connector recipe: ${connector}. Run \`fabric-harness add\` to list recipes.`);
|
|
800
|
+
const recipePath = await findConnectorRecipe(recipeFile);
|
|
801
|
+
if (!print) {
|
|
802
|
+
console.log(`Connector recipe: ${connector}`);
|
|
803
|
+
console.log(recipePath);
|
|
804
|
+
console.log(`\nRun \`fabric-harness add ${connector} --print\` to print the implementation guide.`);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
console.log(await fs.readFile(recipePath, 'utf8'));
|
|
808
|
+
}
|
|
809
|
+
async function findConnectorRecipe(file) {
|
|
810
|
+
let current = process.cwd();
|
|
811
|
+
while (true) {
|
|
812
|
+
const candidate = path.join(current, 'connectors', file);
|
|
813
|
+
try {
|
|
814
|
+
await fs.access(candidate);
|
|
815
|
+
return candidate;
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
// keep walking up
|
|
819
|
+
}
|
|
820
|
+
const parent = path.dirname(current);
|
|
821
|
+
if (parent === current)
|
|
822
|
+
break;
|
|
823
|
+
current = parent;
|
|
824
|
+
}
|
|
825
|
+
const fromCli = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'connectors', file);
|
|
826
|
+
try {
|
|
827
|
+
await fs.access(fromCli);
|
|
828
|
+
return fromCli;
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
throw new Error(`Connector recipe file not found: ${file}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async function verifyCommand(args, kind) {
|
|
835
|
+
const [target, ...rest] = args;
|
|
836
|
+
if (!target || target.startsWith('-'))
|
|
837
|
+
throw new Error(`Missing required <build-dir-or-${kind}> argument.`);
|
|
838
|
+
if (rest.length > 0)
|
|
839
|
+
throw new Error(`Unknown argument: ${rest[0]}`);
|
|
840
|
+
const result = kind === 'attestation' ? await verifyAttestation(target) : await verifyProvenance(target);
|
|
841
|
+
printResult(result);
|
|
842
|
+
}
|
|
843
|
+
async function inspectCommand(args) {
|
|
844
|
+
const [sessionId, ...rest] = args;
|
|
845
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
846
|
+
throw new Error('Missing required <session-id> argument for inspect.');
|
|
847
|
+
if (rest.length > 0)
|
|
848
|
+
throw new Error(`Unknown argument: ${rest[0]}`);
|
|
849
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
850
|
+
const data = await inspectSessionFromStore(store, sessionId);
|
|
851
|
+
if (!data)
|
|
852
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
853
|
+
printResult(data);
|
|
854
|
+
}
|
|
855
|
+
async function replayCommand(args) {
|
|
856
|
+
const [sessionId, ...rest] = args;
|
|
857
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
858
|
+
throw new Error('Missing required <session-id> argument for replay.');
|
|
859
|
+
if (rest.length > 0)
|
|
860
|
+
throw new Error(`Unknown argument: ${rest[0]}`);
|
|
861
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
862
|
+
const replay = await inspectReplayFromStore(store, sessionId);
|
|
863
|
+
if (!replay)
|
|
864
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
865
|
+
console.log(`Replay inspection for session ${replay.sessionId}`);
|
|
866
|
+
console.log('');
|
|
867
|
+
console.log(`Active path entries: ${replay.activePath.length}`);
|
|
868
|
+
for (const entry of replay.activePath) {
|
|
869
|
+
console.log(`${entry.timestamp} ${entry.type} ${entry.id}${entry.parentId ? ` <- ${entry.parentId}` : ''}`);
|
|
870
|
+
}
|
|
871
|
+
console.log('');
|
|
872
|
+
if (replay.latestCompaction) {
|
|
873
|
+
console.log(`Latest compaction: ${replay.latestCompaction.id}`);
|
|
874
|
+
console.log(formatInline(replay.latestCompaction.data ?? {}));
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
console.log('Latest compaction: none');
|
|
878
|
+
}
|
|
879
|
+
console.log('');
|
|
880
|
+
console.log(`Model context messages: ${replay.modelMessages.length}`);
|
|
881
|
+
for (const message of replay.modelMessages) {
|
|
882
|
+
const name = message.name ? ` name=${message.name}` : '';
|
|
883
|
+
console.log(`${message.role}${name}: ${message.content.replace(/\s+/g, ' ').slice(0, 240)}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
async function doctorCommand(args) {
|
|
887
|
+
let target;
|
|
888
|
+
let model;
|
|
889
|
+
let checkTools = false;
|
|
890
|
+
let live = false;
|
|
891
|
+
let jsonOutput = false;
|
|
892
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
893
|
+
const arg = args[index];
|
|
894
|
+
if (arg === '--target') {
|
|
895
|
+
const value = args[++index];
|
|
896
|
+
if (value !== 'node' && value !== 'temporal-worker')
|
|
897
|
+
throw new Error('--target for doctor must be node or temporal-worker.');
|
|
898
|
+
target = value;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (arg === '--model') {
|
|
902
|
+
model = args[++index];
|
|
903
|
+
if (!model)
|
|
904
|
+
throw new Error('Missing value for --model.');
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (arg === '--tools') {
|
|
908
|
+
checkTools = true;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (arg === '--live') {
|
|
912
|
+
live = true;
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
if (arg === '--json') {
|
|
916
|
+
jsonOutput = true;
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
920
|
+
}
|
|
921
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
922
|
+
const checks = [];
|
|
923
|
+
checks.push({ name: 'workspace', ok: true, message: workspaceRoot });
|
|
924
|
+
let config = {};
|
|
925
|
+
try {
|
|
926
|
+
config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
927
|
+
const configuredTarget = target ?? config.run?.target ?? 'node';
|
|
928
|
+
checks.push({ name: 'run-target', ok: true, message: configuredTarget });
|
|
929
|
+
const doctorModel = model ?? process.env.FABRIC_MODEL ?? config.run?.model ?? config.agent?.model;
|
|
930
|
+
const resolved = resolveModelProvider({ ...(doctorModel !== undefined ? { model: doctorModel } : {}), ...(config.agent?.providers !== undefined ? { providers: config.agent.providers } : {}) });
|
|
931
|
+
checks.push({ name: 'model', ok: Boolean(resolved.provider), message: resolved.provider ? `${resolved.providerName}/${resolved.modelId}` : 'model disabled' });
|
|
932
|
+
for (const warning of resolved.warnings)
|
|
933
|
+
checks.push({ name: 'model-warning', ok: true, message: warning });
|
|
934
|
+
if (live && resolved.provider) {
|
|
935
|
+
const response = await resolved.provider.generate({ ...(resolved.model !== undefined ? { model: resolved.model } : {}), messages: [{ role: 'user', content: 'Say exactly: fabric harness doctor ok' }] });
|
|
936
|
+
checks.push({ name: 'model-live', ok: Boolean(response.message?.content || response.toolCalls?.length), message: response.message?.content?.slice(0, 120) ?? `${response.toolCalls?.length ?? 0} tool calls` });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
checks.push({ name: 'model', ok: false, message: error instanceof Error ? error.message : String(error) });
|
|
941
|
+
}
|
|
942
|
+
try {
|
|
943
|
+
const store = await createConfiguredSessionStore({ workspaceRoot, ...(config.store ? { config: config.store } : {}), env: process.env });
|
|
944
|
+
await store.save({ id: 'doctor-write-test', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), entries: [], events: [] });
|
|
945
|
+
const message = config.store?.backend ? config.store.backend : store instanceof FileSessionStore ? store.sessionsDir : 'default';
|
|
946
|
+
checks.push({ name: 'session-store', ok: true, message });
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
checks.push({ name: 'session-store', ok: false, message: error instanceof Error ? error.message : String(error) });
|
|
950
|
+
}
|
|
951
|
+
if (checkTools) {
|
|
952
|
+
try {
|
|
953
|
+
const sandbox = new EmptySandboxEnv();
|
|
954
|
+
const schemas = toolsToModelSchemas(createBuiltinTools(sandbox));
|
|
955
|
+
for (const schema of schemas) {
|
|
956
|
+
const input = schema.inputSchema;
|
|
957
|
+
if (input?.type !== 'object' || typeof input.properties !== 'object' || input.properties === null)
|
|
958
|
+
throw new Error(`Invalid schema for tool ${schema.name}`);
|
|
959
|
+
}
|
|
960
|
+
checks.push({ name: 'tools', ok: true, message: `${schemas.length} tool schemas valid` });
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
checks.push({ name: 'tools', ok: false, message: error instanceof Error ? error.message : String(error) });
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if ((target ?? config.run?.target) === 'temporal-worker') {
|
|
967
|
+
const address = process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? 'localhost:7233';
|
|
968
|
+
const taskQueue = process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ?? 'fabric-harness';
|
|
969
|
+
checks.push({ name: 'temporal', ok: true, message: `configured address ${address}, taskQueue ${taskQueue}; start a worker/server for live validation` });
|
|
970
|
+
}
|
|
971
|
+
if (jsonOutput) {
|
|
972
|
+
printResult({ ok: checks.every((check) => check.ok), checks });
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
for (const check of checks)
|
|
976
|
+
console.log(`${check.ok ? '✓' : '✗'} ${check.name}: ${check.message}`);
|
|
977
|
+
if (!checks.every((check) => check.ok))
|
|
978
|
+
process.exitCode = 1;
|
|
979
|
+
}
|
|
980
|
+
async function agentsCommand(args) {
|
|
981
|
+
const jsonOutput = args.includes('--json');
|
|
982
|
+
const unknown = args.find((arg) => arg !== '--json');
|
|
983
|
+
if (unknown)
|
|
984
|
+
throw new Error(`Unknown argument: ${unknown}`);
|
|
985
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
986
|
+
const agents = await listAgentSummaries(workspaceRoot);
|
|
987
|
+
if (jsonOutput) {
|
|
988
|
+
printResult({ agents });
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (agents.length === 0) {
|
|
992
|
+
console.log('No agents found.');
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
for (const item of agents)
|
|
996
|
+
console.log(`${item.name.padEnd(20)} ${item.description ?? ''}`.trimEnd());
|
|
997
|
+
}
|
|
998
|
+
async function describeCommand(args) {
|
|
999
|
+
const [agentName, ...rest] = args;
|
|
1000
|
+
if (!agentName || agentName.startsWith('-'))
|
|
1001
|
+
throw new Error('Missing required <agent> argument for describe.');
|
|
1002
|
+
const jsonOutput = rest.includes('--json');
|
|
1003
|
+
const unknown = rest.find((arg) => arg !== '--json');
|
|
1004
|
+
if (unknown)
|
|
1005
|
+
throw new Error(`Unknown argument: ${unknown}`);
|
|
1006
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
1007
|
+
const agentPath = await resolveAgentPath(workspaceRoot, agentName);
|
|
1008
|
+
const description = await describeAgentFile({ workspaceRoot, agentPath });
|
|
1009
|
+
if (jsonOutput) {
|
|
1010
|
+
printResult(description);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
console.log(`Agent: ${description.name}`);
|
|
1014
|
+
console.log(`Path: ${path.relative(workspaceRoot, description.path)}`);
|
|
1015
|
+
if (description.description)
|
|
1016
|
+
console.log(`Description: ${description.description}`);
|
|
1017
|
+
if (description.model)
|
|
1018
|
+
console.log(`Default model: ${description.model}`);
|
|
1019
|
+
if (description.target)
|
|
1020
|
+
console.log(`Default target: ${description.target}`);
|
|
1021
|
+
console.log('');
|
|
1022
|
+
console.log('Input:');
|
|
1023
|
+
printSchema(description.inputSchema, ' ');
|
|
1024
|
+
console.log('');
|
|
1025
|
+
console.log('Output:');
|
|
1026
|
+
printSchema(description.outputSchema, ' ');
|
|
1027
|
+
console.log('');
|
|
1028
|
+
console.log('Examples:');
|
|
1029
|
+
const input = description.inputSchema;
|
|
1030
|
+
const firstField = input?.type === 'object' ? Object.keys(input.properties ?? {})[0] : undefined;
|
|
1031
|
+
if (firstField)
|
|
1032
|
+
console.log(` fabric-harness run ${agentName} --${firstField} "..."`);
|
|
1033
|
+
console.log(` fabric-harness run ${agentName} --payload '{"${firstField ?? 'input'}":"..."}'`);
|
|
1034
|
+
}
|
|
1035
|
+
function printSchema(value, indent) {
|
|
1036
|
+
if (!value) {
|
|
1037
|
+
console.log(`${indent}unknown`);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const schema = value;
|
|
1041
|
+
if (schema.type === 'object') {
|
|
1042
|
+
const required = new Set(schema.required ?? []);
|
|
1043
|
+
const properties = schema.properties ?? {};
|
|
1044
|
+
if (Object.keys(properties).length === 0) {
|
|
1045
|
+
console.log(`${indent}object`);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
1049
|
+
const field = property;
|
|
1050
|
+
const req = required.has(key) ? 'required' : 'optional';
|
|
1051
|
+
const enumText = field.enum ? ` enum=${field.enum.join('|')}` : '';
|
|
1052
|
+
const desc = field.description ? ` - ${field.description}` : '';
|
|
1053
|
+
console.log(`${indent}${key.padEnd(16)} ${(field.type ?? 'unknown').padEnd(8)} ${req}${enumText}${desc}`);
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const enumText = schema.enum ? ` enum=${schema.enum.join('|')}` : '';
|
|
1058
|
+
const desc = schema.description ? ` - ${schema.description}` : '';
|
|
1059
|
+
console.log(`${indent}${schema.type ?? 'unknown'}${enumText}${desc}`);
|
|
1060
|
+
}
|
|
1061
|
+
async function sessionsCommand(args) {
|
|
1062
|
+
if (args.length > 0)
|
|
1063
|
+
throw new Error(`Unknown argument: ${args[0]}`);
|
|
1064
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
1065
|
+
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
1066
|
+
const store = await createConfiguredSessionStore({ workspaceRoot, ...(config.store ? { config: config.store } : {}), env: process.env });
|
|
1067
|
+
const sessions = await listSessionSummariesFromStore(store);
|
|
1068
|
+
if (sessions.length === 0) {
|
|
1069
|
+
console.log('No sessions found.');
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
for (const session of sessions) {
|
|
1073
|
+
const agent = session.agentId ? ` agent=${session.agentId}` : '';
|
|
1074
|
+
const error = session.latestError ? ` error=${session.latestError}` : '';
|
|
1075
|
+
console.log(`${session.updatedAt} ${session.id}${agent} entries=${session.entries} events=${session.events} approvals=${session.pendingApprovals}/${session.terminalApprovals}${error}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
async function loadConfiguredStoreForWorkspace() {
|
|
1079
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
1080
|
+
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
1081
|
+
const store = await createConfiguredSessionStore({ workspaceRoot, ...(config.store ? { config: config.store } : {}), env: process.env });
|
|
1082
|
+
return { workspaceRoot, config, store };
|
|
1083
|
+
}
|
|
1084
|
+
async function buildsCommand(args) {
|
|
1085
|
+
if (args.length > 0)
|
|
1086
|
+
throw new Error(`Unknown argument: ${args[0]}`);
|
|
1087
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
1088
|
+
const builds = await listBuilds(workspaceRoot);
|
|
1089
|
+
if (builds.length === 0) {
|
|
1090
|
+
console.log('No builds found.');
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
for (const build of builds) {
|
|
1094
|
+
const entrypoint = build.entrypoint ? ` entry=${build.entrypoint}` : '';
|
|
1095
|
+
console.log(`${build.createdAt} ${build.target}${entrypoint} agents=${build.agents} roles=${build.roles} skills=${build.skills} files=${build.files}`);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async function approvalsCommand(args) {
|
|
1099
|
+
const [sessionId, ...rest] = args;
|
|
1100
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
1101
|
+
throw new Error('Missing required <session-id> argument for approvals.');
|
|
1102
|
+
const pendingOnly = rest.includes('--pending');
|
|
1103
|
+
const stateView = rest.includes('--state');
|
|
1104
|
+
const unknown = rest.find((arg) => arg !== '--pending' && arg !== '--state');
|
|
1105
|
+
if (unknown)
|
|
1106
|
+
throw new Error(`Unknown argument: ${unknown}`);
|
|
1107
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1108
|
+
if (stateView) {
|
|
1109
|
+
const states = (await listApprovalStatesFromStore(store, sessionId)).filter((approval) => !pendingOnly || approval.status === 'pending');
|
|
1110
|
+
if (states.length === 0) {
|
|
1111
|
+
console.log(`No approvals found for session ${sessionId}.`);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
for (const state of states) {
|
|
1115
|
+
const actors = state.votes.map((vote) => `${typeof vote.actor === 'string' ? vote.actor : vote.actor.id}:${vote.decision}`).join(',') || 'none';
|
|
1116
|
+
const resolved = state.resolvedAt ? ` resolved=${state.resolvedAt}` : '';
|
|
1117
|
+
console.log(`${state.status} ${state.id} approvals=${state.approvedCount}/${state.requiredApprovals} denials=${state.deniedCount} votes=${actors}${resolved} subject=${state.request.subject}`);
|
|
1118
|
+
}
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const approvals = (await listApprovalsFromStore(store, sessionId)).filter((approval) => !pendingOnly || approval.status === 'pending');
|
|
1122
|
+
if (approvals.length === 0) {
|
|
1123
|
+
console.log(`No approvals found for session ${sessionId}.`);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
for (const approval of approvals) {
|
|
1127
|
+
const resolved = approval.resolvedAt ? ` resolved=${approval.resolvedAt}` : '';
|
|
1128
|
+
const risk = approval.risk ? ` risk=${approval.risk}` : '';
|
|
1129
|
+
const pattern = approval.matchedPattern ? ` pattern=${approval.matchedPattern}` : '';
|
|
1130
|
+
const env = approval.envKeys?.length ? ` env=${approval.envKeys.join(',')}` : '';
|
|
1131
|
+
const paths = approval.affectedPaths?.length ? ` paths=${approval.affectedPaths.join(',')}` : '';
|
|
1132
|
+
console.log(`${approval.requestedAt} ${approval.status} ${approval.id} ${approval.kind}:${approval.subject}${risk}${pattern}${env}${paths}${resolved} reason=${approval.reason}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async function resolveApprovalCommand(args, decision) {
|
|
1136
|
+
const [sessionId, approvalId, ...rest] = args;
|
|
1137
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
1138
|
+
throw new Error(`Missing required <session-id> argument for ${decision}.`);
|
|
1139
|
+
if (!approvalId || approvalId.startsWith('-'))
|
|
1140
|
+
throw new Error(`Missing required <approval-id> argument for ${decision}.`);
|
|
1141
|
+
let reason;
|
|
1142
|
+
let actor = 'cli';
|
|
1143
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
1144
|
+
const arg = rest[index];
|
|
1145
|
+
if (arg === '--reason') {
|
|
1146
|
+
reason = rest[++index];
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
if (arg === '--actor') {
|
|
1150
|
+
actor = rest[++index] ?? actor;
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
1154
|
+
}
|
|
1155
|
+
const { workspaceRoot, store } = await loadConfiguredStoreForWorkspace();
|
|
1156
|
+
const resolved = await resolveApprovalInStore(store, sessionId, approvalId, decision, reason, actor);
|
|
1157
|
+
await signalTemporalApprovalIfNeeded(workspaceRoot, sessionId, resolved, decision, reason);
|
|
1158
|
+
printResult(resolved);
|
|
1159
|
+
}
|
|
1160
|
+
async function signalTemporalApprovalIfNeeded(workspaceRoot, sessionId, approval, decision, reason) {
|
|
1161
|
+
if (!approval.workflowId || (approval.status !== 'approved' && approval.status !== 'denied'))
|
|
1162
|
+
return;
|
|
1163
|
+
const config = await loadFabricHarnessConfig({ workspaceRoot });
|
|
1164
|
+
try {
|
|
1165
|
+
const namespace = process.env.FABRIC_TEMPORAL_NAMESPACE ?? config.temporal?.namespace;
|
|
1166
|
+
const client = await createTemporalClient({
|
|
1167
|
+
mode: 'temporal',
|
|
1168
|
+
address: process.env.FABRIC_TEMPORAL_ADDRESS ?? config.temporal?.address ?? 'localhost:7233',
|
|
1169
|
+
taskQueue: process.env.FABRIC_TEMPORAL_TASK_QUEUE ?? config.temporal?.taskQueue ?? 'fabric-harness',
|
|
1170
|
+
...(namespace ? { namespace } : {}),
|
|
1171
|
+
...(config.temporal?.apiKey ? { apiKey: config.temporal.apiKey } : {}),
|
|
1172
|
+
...(config.temporal?.apiKeyEnv ? { apiKeyEnv: config.temporal.apiKeyEnv } : {}),
|
|
1173
|
+
...(config.temporal?.tls ? { tls: config.temporal.tls } : {}),
|
|
1174
|
+
});
|
|
1175
|
+
try {
|
|
1176
|
+
await client.signalWorkflowApproval(approval.workflowId, { approvalId: approval.id, decision, ...(reason ?? approval.resolutionReason ? { reason: reason ?? approval.resolutionReason } : {}) });
|
|
1177
|
+
console.error(`Signaled Temporal workflow ${approval.workflowId} for approval ${approval.id}.`);
|
|
1178
|
+
}
|
|
1179
|
+
finally {
|
|
1180
|
+
await client.close();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
catch (error) {
|
|
1184
|
+
console.warn(`Warning: approval ${approval.id} was recorded but Temporal workflow ${approval.workflowId} was not signaled: ${error instanceof Error ? error.message : String(error)}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async function checkpointsCommand(args) {
|
|
1188
|
+
const [sessionId, ...rest] = args;
|
|
1189
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
1190
|
+
throw new Error('Missing required <session-id> argument for checkpoints.');
|
|
1191
|
+
if (rest.length > 0)
|
|
1192
|
+
throw new Error(`Unknown argument: ${rest[0]}`);
|
|
1193
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1194
|
+
const checkpoints = await listCheckpointsFromStore(store, sessionId);
|
|
1195
|
+
if (checkpoints.length === 0) {
|
|
1196
|
+
console.log(`No checkpoints found for session ${sessionId}.`);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
for (const checkpoint of checkpoints) {
|
|
1200
|
+
const snapshot = checkpoint.snapshotId ? ` snapshot=${checkpoint.snapshotId}` : '';
|
|
1201
|
+
console.log(`${checkpoint.timestamp} ${checkpoint.label} entry=${checkpoint.entryId}${snapshot} restored=${checkpoint.restoredCount}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
async function compactCommand(args) {
|
|
1205
|
+
const [sessionId, ...rest] = args;
|
|
1206
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
1207
|
+
throw new Error('Missing required <session-id> argument for compact.');
|
|
1208
|
+
let keepRecentEntries;
|
|
1209
|
+
let summary;
|
|
1210
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
1211
|
+
const arg = rest[index];
|
|
1212
|
+
if (arg === '--keep') {
|
|
1213
|
+
const value = rest[++index];
|
|
1214
|
+
keepRecentEntries = value ? Number(value) : undefined;
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (arg === '--summary') {
|
|
1218
|
+
summary = rest[++index];
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
1222
|
+
}
|
|
1223
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1224
|
+
const options = {};
|
|
1225
|
+
if (keepRecentEntries !== undefined)
|
|
1226
|
+
options.keepRecentEntries = keepRecentEntries;
|
|
1227
|
+
if (summary !== undefined)
|
|
1228
|
+
options.summary = summary;
|
|
1229
|
+
printResult(await compactSessionInStore(store, sessionId, options));
|
|
1230
|
+
}
|
|
1231
|
+
async function logsCommand(args) {
|
|
1232
|
+
const [sessionId, ...rest] = args;
|
|
1233
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
1234
|
+
throw new Error('Missing required <session-id> argument for logs.');
|
|
1235
|
+
const showEvents = rest.includes('--events');
|
|
1236
|
+
const unknown = rest.find((arg) => arg !== '--events');
|
|
1237
|
+
if (unknown)
|
|
1238
|
+
throw new Error(`Unknown argument: ${unknown}`);
|
|
1239
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1240
|
+
const data = await inspectSessionFromStore(store, sessionId);
|
|
1241
|
+
if (!data)
|
|
1242
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1243
|
+
console.log(`Session ${data.id}`);
|
|
1244
|
+
console.log(`Created: ${data.createdAt}`);
|
|
1245
|
+
console.log(`Updated: ${data.updatedAt}`);
|
|
1246
|
+
if (data.agentId)
|
|
1247
|
+
console.log(`Agent: ${data.agentId}`);
|
|
1248
|
+
console.log('');
|
|
1249
|
+
const entries = showEvents ? data.events ?? [] : data.entries ?? [];
|
|
1250
|
+
if (entries.length === 0) {
|
|
1251
|
+
console.log(showEvents ? 'No events.' : 'No structured entries.');
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
for (const item of entries) {
|
|
1255
|
+
const timestamp = 'timestamp' in item ? item.timestamp : '';
|
|
1256
|
+
const type = 'type' in item ? item.type : 'unknown';
|
|
1257
|
+
const dataText = 'data' in item && item.data !== undefined ? ` ${formatInline(item.data)}` : '';
|
|
1258
|
+
console.log(`${timestamp} ${type}${dataText}`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
async function artifactsCommand(args) {
|
|
1262
|
+
const [sessionId, ...rest] = args;
|
|
1263
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
1264
|
+
throw new Error('Missing required <session-id> argument for artifacts.');
|
|
1265
|
+
const json = rest.includes('--json');
|
|
1266
|
+
const unknown = rest.find((arg) => arg !== '--json');
|
|
1267
|
+
if (unknown)
|
|
1268
|
+
throw new Error(`Unknown argument: ${unknown}`);
|
|
1269
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1270
|
+
const artifacts = await (store.listArtifacts?.(sessionId) ?? Promise.resolve([]));
|
|
1271
|
+
if (json)
|
|
1272
|
+
return printResult(artifacts);
|
|
1273
|
+
if (artifacts.length === 0) {
|
|
1274
|
+
console.log(`No artifacts found for session ${sessionId}.`);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
for (const artifact of artifacts)
|
|
1278
|
+
console.log(`${artifact.id} ${artifact.name} ${artifact.size} bytes ${artifact.contentType ?? 'application/octet-stream'}`);
|
|
1279
|
+
}
|
|
1280
|
+
async function artifactCommand(args) {
|
|
1281
|
+
const [subcommand, sessionId, artifactIdOrName, ...rest] = args;
|
|
1282
|
+
if (subcommand !== 'get')
|
|
1283
|
+
throw new Error('Usage: fabric-harness artifact get <session-id> <artifact-id-or-name> [--out <path>]');
|
|
1284
|
+
if (!sessionId || !artifactIdOrName)
|
|
1285
|
+
throw new Error('Usage: fabric-harness artifact get <session-id> <artifact-id-or-name> [--out <path>]');
|
|
1286
|
+
let out;
|
|
1287
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
1288
|
+
const arg = rest[index];
|
|
1289
|
+
if (arg === '--out') {
|
|
1290
|
+
out = rest[++index];
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
1294
|
+
}
|
|
1295
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1296
|
+
const artifact = await store.getArtifact?.(sessionId, artifactIdOrName);
|
|
1297
|
+
if (!artifact)
|
|
1298
|
+
throw new Error(`Artifact not found: ${artifactIdOrName}`);
|
|
1299
|
+
if (out) {
|
|
1300
|
+
const outPath = path.resolve(process.cwd(), out);
|
|
1301
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
1302
|
+
await fs.writeFile(outPath, artifact.content);
|
|
1303
|
+
console.log(outPath);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
process.stdout.write(Buffer.from(artifact.content));
|
|
1307
|
+
}
|
|
1308
|
+
async function metricsCommand(args) {
|
|
1309
|
+
const [sessionId, ...rest] = args;
|
|
1310
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
1311
|
+
throw new Error('Missing required <session-id> argument for metrics.');
|
|
1312
|
+
const json = rest.includes('--json');
|
|
1313
|
+
const unknown = rest.find((arg) => arg !== '--json');
|
|
1314
|
+
if (unknown)
|
|
1315
|
+
throw new Error(`Unknown argument: ${unknown}`);
|
|
1316
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1317
|
+
const metrics = await getMetricsFromStore(store, sessionId);
|
|
1318
|
+
if (!metrics)
|
|
1319
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1320
|
+
if (json)
|
|
1321
|
+
return printResult(metrics);
|
|
1322
|
+
console.log(`Session ${metrics.sessionId}`);
|
|
1323
|
+
console.log(`Model attempts: ${metrics.modelAttempts}`);
|
|
1324
|
+
console.log(`Tokens: input=${metrics.inputTokens} output=${metrics.outputTokens} total=${metrics.totalTokens}`);
|
|
1325
|
+
if (metrics.costUsd !== undefined)
|
|
1326
|
+
console.log(`Cost: $${metrics.costUsd.toFixed(6)}`);
|
|
1327
|
+
console.log(`Tools: calls=${metrics.toolCalls} durationMs=${metrics.toolDurationMs}`);
|
|
1328
|
+
console.log(`Shell: commands=${metrics.shellCommands} durationMs=${metrics.shellDurationMs}`);
|
|
1329
|
+
console.log(`Artifacts: ${metrics.artifacts}`);
|
|
1330
|
+
}
|
|
1331
|
+
async function tasksCommand(args) {
|
|
1332
|
+
const [sessionId, ...rest] = args;
|
|
1333
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
1334
|
+
throw new Error('Missing required <session-id> argument for tasks.');
|
|
1335
|
+
const json = rest.includes('--json');
|
|
1336
|
+
const unknown = rest.find((arg) => arg !== '--json');
|
|
1337
|
+
if (unknown)
|
|
1338
|
+
throw new Error(`Unknown argument: ${unknown}`);
|
|
1339
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1340
|
+
const tasks = await listTasksFromStore(store, sessionId);
|
|
1341
|
+
if (json)
|
|
1342
|
+
return printResult(tasks);
|
|
1343
|
+
if (tasks.length === 0) {
|
|
1344
|
+
console.log(`No tasks found for session ${sessionId}.`);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
for (const task of tasks) {
|
|
1348
|
+
const checkpointText = task.checkpoints.length ? ` checkpoints=${task.checkpoints.length}` : '';
|
|
1349
|
+
const errorText = task.error ? ` error=${truncateInline(task.error)}` : '';
|
|
1350
|
+
console.log(`${task.id} ${task.status} started=${task.startedAt} updated=${task.updatedAt}${checkpointText}${errorText}`);
|
|
1351
|
+
if (task.text)
|
|
1352
|
+
console.log(` ${truncateInline(task.text)}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
async function taskCommand(args) {
|
|
1356
|
+
const [sessionId, taskId, ...rest] = args;
|
|
1357
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
1358
|
+
throw new Error('Missing required <session-id> argument for task.');
|
|
1359
|
+
if (!taskId || taskId.startsWith('-'))
|
|
1360
|
+
throw new Error('Missing required <task-id> argument for task.');
|
|
1361
|
+
const json = rest.includes('--json');
|
|
1362
|
+
const unknown = rest.find((arg) => arg !== '--json');
|
|
1363
|
+
if (unknown)
|
|
1364
|
+
throw new Error(`Unknown argument: ${unknown}`);
|
|
1365
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1366
|
+
const task = await getTaskFromStore(store, sessionId, taskId);
|
|
1367
|
+
if (!task)
|
|
1368
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
1369
|
+
if (json)
|
|
1370
|
+
return printResult(task);
|
|
1371
|
+
console.log(`Task ${task.id}`);
|
|
1372
|
+
console.log(`Session: ${task.sessionId}`);
|
|
1373
|
+
console.log(`Status: ${task.status}`);
|
|
1374
|
+
console.log(`Started: ${task.startedAt}`);
|
|
1375
|
+
console.log(`Updated: ${task.updatedAt}`);
|
|
1376
|
+
if (task.completedAt)
|
|
1377
|
+
console.log(`Done: ${task.completedAt}`);
|
|
1378
|
+
if (task.failedAt)
|
|
1379
|
+
console.log(`Failed: ${task.failedAt}`);
|
|
1380
|
+
if (task.cancelledAt)
|
|
1381
|
+
console.log(`Cancel: ${task.cancelledAt}`);
|
|
1382
|
+
if (task.agent)
|
|
1383
|
+
console.log(`Agent: ${task.agent}`);
|
|
1384
|
+
if (task.error)
|
|
1385
|
+
console.log(`Error: ${task.error}`);
|
|
1386
|
+
if (task.text)
|
|
1387
|
+
console.log(`Text: ${task.text}`);
|
|
1388
|
+
if (task.checkpoints.length) {
|
|
1389
|
+
console.log('Checkpoints:');
|
|
1390
|
+
for (const checkpoint of task.checkpoints)
|
|
1391
|
+
console.log(` ${checkpoint.timestamp} ${checkpoint.phase ?? 'checkpoint'} ${checkpoint.checkpointId ?? checkpoint.entryId}`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
async function cancelTaskCommand(args) {
|
|
1395
|
+
const [sessionId, taskId, ...rest] = args;
|
|
1396
|
+
if (!sessionId || sessionId.startsWith('-'))
|
|
1397
|
+
throw new Error('Missing required <session-id> argument for cancel-task.');
|
|
1398
|
+
if (!taskId || taskId.startsWith('-'))
|
|
1399
|
+
throw new Error('Missing required <task-id> argument for cancel-task.');
|
|
1400
|
+
let actor = 'cli';
|
|
1401
|
+
let reason;
|
|
1402
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
1403
|
+
const arg = rest[index];
|
|
1404
|
+
if (arg === '--actor') {
|
|
1405
|
+
actor = rest[++index] ?? actor;
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
if (arg === '--reason') {
|
|
1409
|
+
reason = rest[++index];
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
1413
|
+
}
|
|
1414
|
+
const { store } = await loadConfiguredStoreForWorkspace();
|
|
1415
|
+
const task = await cancelTaskInStore(store, sessionId, taskId, reason, actor);
|
|
1416
|
+
console.log(`Task ${task.id} cancelled.`);
|
|
1417
|
+
}
|
|
1418
|
+
function truncateInline(text) {
|
|
1419
|
+
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
1420
|
+
}
|
|
1421
|
+
function formatInline(value) {
|
|
1422
|
+
const text = JSON.stringify(value);
|
|
1423
|
+
if (!text)
|
|
1424
|
+
return '';
|
|
1425
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
1426
|
+
}
|
|
1427
|
+
async function main(argv) {
|
|
1428
|
+
const [command, ...args] = argv;
|
|
1429
|
+
if (!command || command === '--help' || command === '-h') {
|
|
1430
|
+
console.log(HELP);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
await loadAutoEnvFiles();
|
|
1434
|
+
if (command === 'run')
|
|
1435
|
+
return runCommand(args);
|
|
1436
|
+
if (command === 'agents')
|
|
1437
|
+
return agentsCommand(args);
|
|
1438
|
+
if (command === 'describe')
|
|
1439
|
+
return describeCommand(args);
|
|
1440
|
+
if (command === 'build')
|
|
1441
|
+
return buildCommand(args);
|
|
1442
|
+
if (command === 'sessions')
|
|
1443
|
+
return sessionsCommand(args);
|
|
1444
|
+
if (command === 'builds')
|
|
1445
|
+
return buildsCommand(args);
|
|
1446
|
+
if (command === 'verify-attestation')
|
|
1447
|
+
return verifyCommand(args, 'attestation');
|
|
1448
|
+
if (command === 'verify-provenance')
|
|
1449
|
+
return verifyCommand(args, 'provenance');
|
|
1450
|
+
if (command === 'doctor')
|
|
1451
|
+
return doctorCommand(args);
|
|
1452
|
+
if (command === 'add')
|
|
1453
|
+
return addCommand(args);
|
|
1454
|
+
if (command === 'inspect')
|
|
1455
|
+
return inspectCommand(args);
|
|
1456
|
+
if (command === 'logs')
|
|
1457
|
+
return logsCommand(args);
|
|
1458
|
+
if (command === 'checkpoints')
|
|
1459
|
+
return checkpointsCommand(args);
|
|
1460
|
+
if (command === 'artifacts')
|
|
1461
|
+
return artifactsCommand(args);
|
|
1462
|
+
if (command === 'artifact')
|
|
1463
|
+
return artifactCommand(args);
|
|
1464
|
+
if (command === 'metrics')
|
|
1465
|
+
return metricsCommand(args);
|
|
1466
|
+
if (command === 'tasks')
|
|
1467
|
+
return tasksCommand(args);
|
|
1468
|
+
if (command === 'task')
|
|
1469
|
+
return taskCommand(args);
|
|
1470
|
+
if (command === 'cancel-task')
|
|
1471
|
+
return cancelTaskCommand(args);
|
|
1472
|
+
if (command === 'compact')
|
|
1473
|
+
return compactCommand(args);
|
|
1474
|
+
if (command === 'replay')
|
|
1475
|
+
return replayCommand(args);
|
|
1476
|
+
if (command === 'approvals')
|
|
1477
|
+
return approvalsCommand(args);
|
|
1478
|
+
if (command === 'approve')
|
|
1479
|
+
return resolveApprovalCommand(args, 'approved');
|
|
1480
|
+
if (command === 'reject')
|
|
1481
|
+
return resolveApprovalCommand(args, 'denied');
|
|
1482
|
+
if (command === 'dev')
|
|
1483
|
+
return devCommand(args);
|
|
1484
|
+
if (command === 'temporal-worker')
|
|
1485
|
+
return temporalWorkerCommand(args);
|
|
1486
|
+
throw new Error(`Unknown command: ${command}`);
|
|
1487
|
+
}
|
|
1488
|
+
try {
|
|
1489
|
+
await main(process.argv.slice(2));
|
|
1490
|
+
}
|
|
1491
|
+
catch (error) {
|
|
1492
|
+
const message = error instanceof SchemaValidationError ? `Invalid payload or result:\n${error.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n')}` : error instanceof Error ? error.message : String(error);
|
|
1493
|
+
console.error(`fabric-harness: ${message}`);
|
|
1494
|
+
console.error('Run fabric-harness --help for usage.');
|
|
1495
|
+
process.exit(1);
|
|
1496
|
+
}
|
|
1497
|
+
//# sourceMappingURL=fabric-harness.js.map
|