@nifrajs/coding-agent 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/SECURITY.md +2 -1
- package/dist/approvals.d.ts +14 -2
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +51 -43
- package/dist/approvals.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -2
- package/dist/cli.js.map +1 -1
- package/dist/context.js +29 -1
- package/dist/context.js.map +1 -1
- package/dist/diff.js +38 -2
- package/dist/diff.js.map +1 -1
- package/dist/handoffs.d.ts.map +1 -1
- package/dist/handoffs.js +12 -4
- package/dist/handoffs.js.map +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +146 -23
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/isolated.js +37 -2
- package/dist/isolated.js.map +1 -1
- package/dist/native.d.ts +12 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +165 -40
- package/dist/native.js.map +1 -1
- package/dist/orchestration/compile.d.ts +7 -0
- package/dist/orchestration/compile.d.ts.map +1 -1
- package/dist/orchestration/compile.js +16 -2
- package/dist/orchestration/compile.js.map +1 -1
- package/dist/orchestration/evidence-store.d.ts.map +1 -1
- package/dist/orchestration/evidence-store.js +9 -4
- package/dist/orchestration/evidence-store.js.map +1 -1
- package/dist/orchestration/host.d.ts +10 -0
- package/dist/orchestration/host.d.ts.map +1 -1
- package/dist/orchestration/host.js +9 -0
- package/dist/orchestration/host.js.map +1 -1
- package/dist/replay.d.ts +4 -0
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +189 -24
- package/dist/replay.js.map +1 -1
- package/dist/rpc.d.ts +10 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +209 -29
- package/dist/rpc.js.map +1 -1
- package/dist/session-migration.d.ts.map +1 -1
- package/dist/session-migration.js +3 -2
- package/dist/session-migration.js.map +1 -1
- package/dist/sessions.d.ts +20 -2
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +269 -60
- package/dist/sessions.js.map +1 -1
- package/dist/subagents.d.ts +30 -0
- package/dist/subagents.d.ts.map +1 -1
- package/dist/subagents.js +118 -15
- package/dist/subagents.js.map +1 -1
- package/dist/verification.d.ts +28 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +119 -3
- package/dist/verification.js.map +1 -1
- package/dist/workflows.d.ts +1 -0
- package/dist/workflows.d.ts.map +1 -1
- package/dist/workflows.js +55 -23
- package/dist/workflows.js.map +1 -1
- package/package.json +9 -5
- package/src/approvals.ts +62 -45
- package/src/cli.ts +11 -2
- package/src/context.ts +32 -1
- package/src/diff.ts +40 -2
- package/src/handoffs.ts +20 -8
- package/src/host.ts +146 -23
- package/src/index.ts +7 -0
- package/src/isolated.ts +39 -2
- package/src/native.ts +213 -39
- package/src/orchestration/compile.ts +28 -2
- package/src/orchestration/evidence-store.ts +9 -3
- package/src/orchestration/host.ts +17 -0
- package/src/replay.ts +206 -25
- package/src/rpc.ts +262 -33
- package/src/session-migration.ts +3 -2
- package/src/sessions.ts +288 -57
- package/src/subagents.ts +147 -17
- package/src/verification.ts +146 -3
- package/src/workflows.ts +47 -27
package/dist/workflows.js
CHANGED
|
@@ -2,6 +2,7 @@ import { publicErrorDetails } from "./errors.js";
|
|
|
2
2
|
/** Bounded orchestration primitives. The kernel knows no provider, UI, or framework package. */
|
|
3
3
|
export class WorkflowRunner {
|
|
4
4
|
options;
|
|
5
|
+
signal;
|
|
5
6
|
values = new Map();
|
|
6
7
|
completed = [];
|
|
7
8
|
steps = 0;
|
|
@@ -11,6 +12,7 @@ export class WorkflowRunner {
|
|
|
11
12
|
maxSteps: options.maxSteps ?? 128,
|
|
12
13
|
maxDepth: options.maxDepth ?? 8,
|
|
13
14
|
};
|
|
15
|
+
this.signal = options.signal ?? new AbortController().signal;
|
|
14
16
|
if (!Number.isSafeInteger(this.options.maxSteps) || this.options.maxSteps < 1)
|
|
15
17
|
throw new RangeError("workflow: maxSteps must be positive");
|
|
16
18
|
if (!Number.isSafeInteger(this.options.maxDepth) || this.options.maxDepth < 1)
|
|
@@ -36,12 +38,12 @@ export class WorkflowRunner {
|
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
|
-
context() {
|
|
41
|
+
context(depth) {
|
|
40
42
|
return {
|
|
41
|
-
signal: this.
|
|
43
|
+
signal: this.signal,
|
|
42
44
|
values: this.values,
|
|
43
45
|
set: (name, value) => this.values.set(name, value),
|
|
44
|
-
run: (step) => this.execute(step, 1),
|
|
46
|
+
run: (step) => this.execute(step, depth + 1),
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
async execute(step, depth) {
|
|
@@ -53,9 +55,9 @@ export class WorkflowRunner {
|
|
|
53
55
|
throw new Error("workflow cancelled");
|
|
54
56
|
switch (step.type) {
|
|
55
57
|
case "task":
|
|
56
|
-
return this.single(step.id, () => step.run(this.context()));
|
|
58
|
+
return this.single(step.id, () => step.run(this.context(depth)));
|
|
57
59
|
case "verify": {
|
|
58
|
-
const result = await this.single(step.id, () => step.run(this.context()));
|
|
60
|
+
const result = await this.single(step.id, () => step.run(this.context(depth)));
|
|
59
61
|
const ok = typeof result === "boolean" ? result : isVerificationResult(result) && result.ok;
|
|
60
62
|
if (!ok)
|
|
61
63
|
throw new Error(`verification failed: ${step.id}`);
|
|
@@ -63,18 +65,18 @@ export class WorkflowRunner {
|
|
|
63
65
|
}
|
|
64
66
|
case "approve": {
|
|
65
67
|
await this.emit({ type: "approval.required", id: step.id, reason: step.reason });
|
|
66
|
-
const approved = await this.single(step.id, () => step.run(this.context()));
|
|
68
|
+
const approved = await this.single(step.id, () => step.run(this.context(depth)));
|
|
67
69
|
if (approved !== true)
|
|
68
70
|
throw new Error(`approval denied: ${step.id}`);
|
|
69
71
|
return approved;
|
|
70
72
|
}
|
|
71
73
|
case "checkpoint": {
|
|
72
|
-
const result = await this.single(step.id, () => step.run(this.context()));
|
|
74
|
+
const result = await this.single(step.id, () => step.run(this.context(depth)));
|
|
73
75
|
await this.emit({ type: "checkpoint.created", id: step.id });
|
|
74
76
|
return result;
|
|
75
77
|
}
|
|
76
78
|
case "handoff":
|
|
77
|
-
return this.single(step.id, () => step.run(this.context()));
|
|
79
|
+
return this.single(step.id, () => step.run(this.context(depth)));
|
|
78
80
|
case "sequence": {
|
|
79
81
|
const results = [];
|
|
80
82
|
for (const child of step.steps)
|
|
@@ -82,11 +84,16 @@ export class WorkflowRunner {
|
|
|
82
84
|
return results;
|
|
83
85
|
}
|
|
84
86
|
case "parallel":
|
|
87
|
+
if (step.steps.length === 0)
|
|
88
|
+
return [];
|
|
85
89
|
return this.parallel(step.steps, step.maxConcurrency ?? step.steps.length, depth);
|
|
86
90
|
case "retry": {
|
|
87
91
|
const attempts = step.attempts ?? 3;
|
|
88
92
|
if (!Number.isSafeInteger(attempts) || attempts < 1 || attempts > 16)
|
|
89
93
|
throw new RangeError("workflow: attempts must be between 1 and 16");
|
|
94
|
+
const backoffMs = step.backoffMs ?? 0;
|
|
95
|
+
if (!Number.isSafeInteger(backoffMs) || backoffMs < 0)
|
|
96
|
+
throw new RangeError("workflow: backoffMs must be a non-negative safe integer");
|
|
90
97
|
let last;
|
|
91
98
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
92
99
|
try {
|
|
@@ -94,14 +101,14 @@ export class WorkflowRunner {
|
|
|
94
101
|
}
|
|
95
102
|
catch (error) {
|
|
96
103
|
last = error;
|
|
97
|
-
if (attempt + 1 < attempts &&
|
|
98
|
-
await delay(Math.min(
|
|
104
|
+
if (attempt + 1 < attempts && backoffMs > 0)
|
|
105
|
+
await delay(Math.min(backoffMs * (attempt + 1), 10_000), this.options.signal);
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
throw last instanceof Error ? last : new Error(String(last));
|
|
102
109
|
}
|
|
103
110
|
case "branch": {
|
|
104
|
-
const selected = await this.single(step.id, () => step.when(this.context()));
|
|
111
|
+
const selected = await this.single(step.id, () => step.when(this.context(depth)));
|
|
105
112
|
return selected === true
|
|
106
113
|
? this.execute(step.then, depth + 1)
|
|
107
114
|
: step.otherwise === undefined
|
|
@@ -134,15 +141,25 @@ export class WorkflowRunner {
|
|
|
134
141
|
throw new RangeError("workflow: maxConcurrency must be positive");
|
|
135
142
|
const outputs = new Array(steps.length);
|
|
136
143
|
let next = 0;
|
|
144
|
+
// The first failure starts no further step, and the group settles only once the steps already
|
|
145
|
+
// running have, so nothing runs on after the workflow reports its failure.
|
|
146
|
+
let failure;
|
|
137
147
|
const worker = async () => {
|
|
138
|
-
|
|
148
|
+
while (failure === undefined) {
|
|
139
149
|
const index = next++;
|
|
140
150
|
if (index >= steps.length)
|
|
141
151
|
return;
|
|
142
|
-
|
|
152
|
+
try {
|
|
153
|
+
outputs[index] = await this.execute(steps[index], depth + 1);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
failure ??= { error };
|
|
157
|
+
}
|
|
143
158
|
}
|
|
144
159
|
};
|
|
145
160
|
await Promise.all(Array.from({ length: Math.min(maxConcurrency, steps.length) }, () => worker()));
|
|
161
|
+
if (failure !== undefined)
|
|
162
|
+
throw failure.error;
|
|
146
163
|
return outputs;
|
|
147
164
|
}
|
|
148
165
|
async emit(event) {
|
|
@@ -151,18 +168,33 @@ export class WorkflowRunner {
|
|
|
151
168
|
}
|
|
152
169
|
async function delay(ms, signal) {
|
|
153
170
|
await new Promise((resolve, reject) => {
|
|
154
|
-
|
|
171
|
+
let settled = false;
|
|
172
|
+
let timer;
|
|
173
|
+
let onAbort;
|
|
174
|
+
const cleanup = () => {
|
|
175
|
+
if (timer !== undefined)
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
if (signal !== undefined && onAbort !== undefined)
|
|
178
|
+
signal.removeEventListener("abort", onAbort);
|
|
179
|
+
};
|
|
180
|
+
const settle = (error) => {
|
|
181
|
+
if (settled)
|
|
182
|
+
return;
|
|
183
|
+
settled = true;
|
|
184
|
+
cleanup();
|
|
185
|
+
if (error === undefined)
|
|
186
|
+
resolve();
|
|
187
|
+
else
|
|
188
|
+
reject(error);
|
|
189
|
+
};
|
|
190
|
+
timer = setTimeout(() => settle(), ms);
|
|
155
191
|
if (signal === undefined)
|
|
156
192
|
return;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
signal.addEventListener("abort", () => {
|
|
163
|
-
clearTimeout(timer);
|
|
164
|
-
reject(new Error("workflow cancelled"));
|
|
165
|
-
}, { once: true });
|
|
193
|
+
onAbort = () => settle(new Error("workflow cancelled"));
|
|
194
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
195
|
+
// Abort can race listener registration; check again after subscribing.
|
|
196
|
+
if (signal.aborted)
|
|
197
|
+
onAbort();
|
|
166
198
|
});
|
|
167
199
|
}
|
|
168
200
|
function isVerificationResult(value) {
|
package/dist/workflows.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflows.js","sourceRoot":"","sources":["../src/workflows.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAsFhD,gGAAgG;AAChG,MAAM,OAAO,cAAc;IACR,OAAO,CACD;IACN,MAAM,GAAG,IAAI,GAAG,EAAmB,CAAA;IACnC,SAAS,GAAa,EAAE,CAAA;IACjC,KAAK,GAAG,CAAC,CAAA;IAEjB,YAAY,UAAiC,EAAE;QAC7C,IAAI,CAAC,OAAO,GAAG;YACb,GAAG,OAAO;YACV,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,GAAG;YACjC,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,CAAC;SAChC,CAAA;QACD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC;YAC3E,MAAM,IAAI,UAAU,CAAC,qCAAqC,CAAC,CAAA;QAC7D,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC;YAC3E,MAAM,IAAI,UAAU,CAAC,qCAAqC,CAAC,CAAA;IAC/D,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,IAAkB;QAC1B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;YAC3B,OAAO;gBACL,EAAE,EAAE,IAAI;gBACR,MAAM,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC5B,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;aAC9C,CAAA;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,kBAAkB,CAChC,KAAK,EACL,iBAAiB,EACjB,IAAI,CAAC,OAAO,CAAC,iBAAiB,KAAK,IAAI,CACxC,CAAA;YACD,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC5B,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;gBAC7C,KAAK,EAAE,OAAO,CAAC,OAAO;gBACtB,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;aACjE,CAAA;QACH,CAAC;IACH,CAAC;IAEO,OAAO;
|
|
1
|
+
{"version":3,"file":"workflows.js","sourceRoot":"","sources":["../src/workflows.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/coding-agent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Standalone, extensible Nifra coding-agent host and CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"workflows"
|
|
13
13
|
],
|
|
14
14
|
"type": "module",
|
|
15
|
+
"nifra": {
|
|
16
|
+
"environment": "backend"
|
|
17
|
+
},
|
|
15
18
|
"main": "./dist/index.js",
|
|
16
19
|
"module": "./dist/index.js",
|
|
17
20
|
"types": "./dist/index.d.ts",
|
|
@@ -67,10 +70,11 @@
|
|
|
67
70
|
"access": "public"
|
|
68
71
|
},
|
|
69
72
|
"dependencies": {
|
|
70
|
-
"@nifrajs/agent": "
|
|
71
|
-
"@nifrajs/agent-
|
|
72
|
-
"@nifrajs/
|
|
73
|
-
"@nifrajs/
|
|
73
|
+
"@nifrajs/agent": "4.0.0",
|
|
74
|
+
"@nifrajs/agent-review": "4.0.0",
|
|
75
|
+
"@nifrajs/agent-protocol": "4.0.0",
|
|
76
|
+
"@nifrajs/jobs": "4.0.0",
|
|
77
|
+
"@nifrajs/pi": "4.0.0"
|
|
74
78
|
},
|
|
75
79
|
"repository": {
|
|
76
80
|
"type": "git",
|
package/src/approvals.ts
CHANGED
|
@@ -52,10 +52,16 @@ export interface ApprovalManagerOptions {
|
|
|
52
52
|
|
|
53
53
|
interface PendingApproval {
|
|
54
54
|
readonly request: ApprovalRequest
|
|
55
|
-
|
|
55
|
+
/** Settles with the decision; it exists before `onRequired` runs, so no decision can miss it. */
|
|
56
|
+
readonly decided: Promise<boolean>
|
|
57
|
+
readonly settle: (approved: boolean) => void
|
|
56
58
|
readonly timer: ReturnType<typeof setTimeout>
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
/** The agent protocol's token alphabet, so every protocol-valid id is accepted here too. */
|
|
62
|
+
const ID_TOKEN = /^[A-Za-z0-9._:/-]{1,128}$/
|
|
63
|
+
const MAX_ACTION_LENGTH = 512
|
|
64
|
+
|
|
59
65
|
/**
|
|
60
66
|
* Small approval broker shared by RPC, Workbench, and workflow extensions.
|
|
61
67
|
*
|
|
@@ -89,38 +95,37 @@ export class ApprovalManager {
|
|
|
89
95
|
|
|
90
96
|
/** Convert a streamed backend approval event into a resolvable pending request. */
|
|
91
97
|
async observe(event: AgentApprovalRequiredEvent): Promise<ApprovalRequest | undefined> {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
const existing = this.pendingApprovals.get(event.approvalId)
|
|
99
|
+
if (existing !== undefined) return existing.request
|
|
100
|
+
const pending = await this.create({
|
|
95
101
|
id: event.approvalId,
|
|
96
102
|
sessionId: event.sessionId,
|
|
97
103
|
turnId: event.turnId,
|
|
98
|
-
|
|
104
|
+
// The protocol bounds text more loosely; a long label must not fail the backend's turn.
|
|
105
|
+
action: event.action.slice(0, MAX_ACTION_LENGTH),
|
|
99
106
|
capability: event.capability,
|
|
100
107
|
...(event.reason === undefined ? {} : { reason: event.reason }),
|
|
101
108
|
})
|
|
109
|
+
return pending?.request
|
|
102
110
|
}
|
|
103
111
|
|
|
104
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Publish a pending approval without waiting for its decision (useful for transports). Returns
|
|
114
|
+
* `undefined` when the broker is full or an approval with the same id is still pending.
|
|
115
|
+
*/
|
|
105
116
|
async offer(
|
|
106
117
|
input: Omit<ApprovalRequest, "createdAt" | "expiresAt">,
|
|
107
118
|
): Promise<ApprovalRequest | undefined> {
|
|
108
|
-
return this.create(input)
|
|
119
|
+
return (await this.create(input))?.request
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Create a host-owned approval that can be awaited by a workflow or extension. Resolves `false`
|
|
124
|
+
* when the broker is full or an approval with the same id is still pending.
|
|
125
|
+
*/
|
|
112
126
|
async request(input: Omit<ApprovalRequest, "createdAt" | "expiresAt">): Promise<boolean> {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const pending = this.pendingApprovals.get(request.id)
|
|
116
|
-
if (pending === undefined) return false
|
|
117
|
-
return new Promise<boolean>((resolve) => {
|
|
118
|
-
// Replace the resolver only for this host-owned request; observed backend requests are
|
|
119
|
-
// intentionally notification-only and are resolved through `resolve`.
|
|
120
|
-
const current = this.pendingApprovals.get(request.id)
|
|
121
|
-
if (current === undefined) return resolve(false)
|
|
122
|
-
this.pendingApprovals.set(request.id, { ...current, resolve })
|
|
123
|
-
})
|
|
127
|
+
const pending = await this.create(input)
|
|
128
|
+
return pending === undefined ? false : pending.decided
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
/**
|
|
@@ -145,49 +150,68 @@ export class ApprovalManager {
|
|
|
145
150
|
code: bound.vector !== coordinate.vector ? "stale_vector" : "identity_mismatch",
|
|
146
151
|
}
|
|
147
152
|
if (!coordinateIsFresh(bound, now)) {
|
|
148
|
-
this.
|
|
153
|
+
this.settle(coordinate.requestId, false, "approval expired")
|
|
149
154
|
return { ok: false, code: "expired" }
|
|
150
155
|
}
|
|
151
156
|
if (nextApprovalState("pending", approved ? "approve" : "deny") === undefined)
|
|
152
157
|
return { ok: false, code: "illegal_transition" }
|
|
153
|
-
const decision = this.
|
|
158
|
+
const decision = this.settle(coordinate.requestId, approved)
|
|
154
159
|
if (decision === undefined) return { ok: false, code: "unknown_boundary" }
|
|
155
160
|
return { ok: true, decision }
|
|
156
161
|
}
|
|
157
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Settle a pending approval by id. An approval opened with a coordinate is approved only through
|
|
165
|
+
* {@link resolveMatched}; this untyped path can still deny it, and returns `undefined` for an
|
|
166
|
+
* approve.
|
|
167
|
+
*/
|
|
158
168
|
resolve(approvalId: string, approved: boolean, reason?: string): ApprovalDecision | undefined {
|
|
169
|
+
if (
|
|
170
|
+
approved === true &&
|
|
171
|
+
this.pendingApprovals.get(approvalId)?.request.coordinate !== undefined
|
|
172
|
+
)
|
|
173
|
+
return undefined
|
|
174
|
+
return this.settle(approvalId, approved, reason)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
close(): void {
|
|
178
|
+
for (const approvalId of this.pendingApprovals.keys())
|
|
179
|
+
this.settle(approvalId, false, "approval manager closed")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private settle(
|
|
183
|
+
approvalId: string,
|
|
184
|
+
approved: boolean,
|
|
185
|
+
reason?: string,
|
|
186
|
+
): ApprovalDecision | undefined {
|
|
159
187
|
const pending = this.pendingApprovals.get(approvalId)
|
|
160
188
|
if (pending === undefined) return undefined
|
|
161
189
|
clearTimeout(pending.timer)
|
|
162
190
|
this.pendingApprovals.delete(approvalId)
|
|
163
|
-
pending.
|
|
191
|
+
pending.settle(approved === true)
|
|
164
192
|
const decision: ApprovalDecision = {
|
|
165
193
|
approvalId,
|
|
166
194
|
approved: approved === true,
|
|
167
195
|
...(reason === undefined ? {} : { reason: reason.slice(0, 512) }),
|
|
168
196
|
at: Date.now(),
|
|
169
197
|
}
|
|
170
|
-
|
|
198
|
+
// A failing broadcast must not become an unhandled rejection in the host.
|
|
199
|
+
Promise.resolve(this.options.onResolved?.(decision)).catch(() => {})
|
|
171
200
|
return decision
|
|
172
201
|
}
|
|
173
202
|
|
|
174
|
-
close(): void {
|
|
175
|
-
for (const approvalId of this.pendingApprovals.keys())
|
|
176
|
-
this.resolve(approvalId, false, "approval manager closed")
|
|
177
|
-
}
|
|
178
|
-
|
|
179
203
|
private async create(
|
|
180
204
|
input: Omit<ApprovalRequest, "createdAt" | "expiresAt">,
|
|
181
|
-
): Promise<
|
|
205
|
+
): Promise<PendingApproval | undefined> {
|
|
182
206
|
if (this.pendingApprovals.size >= this.options.maxPending) return undefined
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
throw new TypeError("approvals: session id is invalid")
|
|
187
|
-
if (!input.action || input.action.length > 512)
|
|
207
|
+
if (!ID_TOKEN.test(input.id)) throw new TypeError("approvals: approval id is invalid")
|
|
208
|
+
if (!ID_TOKEN.test(input.sessionId)) throw new TypeError("approvals: session id is invalid")
|
|
209
|
+
if (!input.action || input.action.length > MAX_ACTION_LENGTH)
|
|
188
210
|
throw new TypeError("approvals: action is empty or too long")
|
|
189
211
|
if (!input.capability || input.capability.length > 128)
|
|
190
212
|
throw new TypeError("approvals: capability is empty or too long")
|
|
213
|
+
// Replacing a pending id would orphan the waiter of the request already holding it.
|
|
214
|
+
if (this.pendingApprovals.has(input.id)) return undefined
|
|
191
215
|
const createdAt = Date.now()
|
|
192
216
|
const request: ApprovalRequest = Object.freeze({
|
|
193
217
|
...input,
|
|
@@ -197,22 +221,15 @@ export class ApprovalManager {
|
|
|
197
221
|
createdAt,
|
|
198
222
|
expiresAt: createdAt + this.options.timeoutMs,
|
|
199
223
|
})
|
|
200
|
-
|
|
224
|
+
const { promise: decided, resolve: settle } = Promise.withResolvers<boolean>()
|
|
201
225
|
const timer = setTimeout(() => {
|
|
202
|
-
this.
|
|
226
|
+
this.settle(request.id, false, "approval timed out")
|
|
203
227
|
}, this.options.timeoutMs)
|
|
204
228
|
;(timer as unknown as { unref?: () => void }).unref?.()
|
|
205
|
-
const pending: PendingApproval = {
|
|
206
|
-
request,
|
|
207
|
-
resolve: (approved) => resolveApproval(approved),
|
|
208
|
-
timer,
|
|
209
|
-
}
|
|
229
|
+
const pending: PendingApproval = { request, decided, settle, timer }
|
|
210
230
|
this.pendingApprovals.set(request.id, pending)
|
|
211
231
|
await this.options.onRequired?.(request)
|
|
212
|
-
|
|
213
|
-
// from the promise below before returning to the workflow.
|
|
214
|
-
resolveApproval = (_approved: boolean): void => {}
|
|
215
|
-
return request
|
|
232
|
+
return pending
|
|
216
233
|
}
|
|
217
234
|
}
|
|
218
235
|
|
package/src/cli.ts
CHANGED
|
@@ -25,10 +25,13 @@ Usage:
|
|
|
25
25
|
nifra-agent [--backend pi|replay] [--cwd <dir>] [--once <prompt>]
|
|
26
26
|
[--json] [--no-session] [--session-dir <dir>] [--session-id <id>]
|
|
27
27
|
[--verify-after-turn check,assure] [--max-repair-attempts <n>]
|
|
28
|
-
[--pi <command>] [--replay <file>]
|
|
28
|
+
[--pi <command>] [--replay <file>] [--extensions]
|
|
29
29
|
nifra-agent --migrate-session <id> --migrate-from <dir> --migrate-to <dir> [--json]
|
|
30
30
|
nifra-agent --rpc [--cwd <dir>] [--host 127.0.0.1] [--port 0] [--expose-error-stacks]
|
|
31
31
|
|
|
32
|
+
--extensions loads and runs .nifra/extensions/** from --cwd. That is the project's own code: pass it
|
|
33
|
+
only for a repository you trust. Without it no extension is loaded.
|
|
34
|
+
|
|
32
35
|
Commands in interactive mode:
|
|
33
36
|
/reload reload Pi or Nifra extensions
|
|
34
37
|
/compact compact the bounded prompt window
|
|
@@ -64,6 +67,7 @@ export interface CliOptions {
|
|
|
64
67
|
readonly migrationTarget?: string
|
|
65
68
|
readonly verifyAfterTurn: readonly ("check" | "assure" | "test")[]
|
|
66
69
|
readonly maxRepairAttempts: number
|
|
70
|
+
readonly extensions: boolean
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
export function parseArgs(args: readonly string[]): CliOptions {
|
|
@@ -72,6 +76,7 @@ export function parseArgs(args: readonly string[]): CliOptions {
|
|
|
72
76
|
let once: string | undefined
|
|
73
77
|
let json = false
|
|
74
78
|
let noSession = false
|
|
79
|
+
let extensions = false
|
|
75
80
|
let piCommand = "pi"
|
|
76
81
|
let replayFile: string | undefined
|
|
77
82
|
let sessionDir: string | undefined
|
|
@@ -93,6 +98,7 @@ export function parseArgs(args: readonly string[]): CliOptions {
|
|
|
93
98
|
else if (arg === "--once" || arg === "--message") once = args[++index]
|
|
94
99
|
else if (arg === "--json") json = true
|
|
95
100
|
else if (arg === "--no-session") noSession = true
|
|
101
|
+
else if (arg === "--extensions") extensions = true
|
|
96
102
|
else if (arg === "--session-dir") sessionDir = resolve(args[++index] ?? "")
|
|
97
103
|
else if (arg === "--session-id") sessionId = args[++index]
|
|
98
104
|
else if (arg === "--migrate-session") migrateSession = args[++index]
|
|
@@ -142,6 +148,7 @@ export function parseArgs(args: readonly string[]): CliOptions {
|
|
|
142
148
|
...(once === undefined ? {} : { once }),
|
|
143
149
|
json,
|
|
144
150
|
noSession,
|
|
151
|
+
extensions,
|
|
145
152
|
piCommand,
|
|
146
153
|
...(replayFile === undefined ? {} : { replayFile }),
|
|
147
154
|
...(sessionDir === undefined ? {} : { sessionDir }),
|
|
@@ -215,7 +222,9 @@ async function main(): Promise<void> {
|
|
|
215
222
|
enableNifraTools: true,
|
|
216
223
|
...(options.sessionDir === undefined ? {} : { sessionDir: options.sessionDir }),
|
|
217
224
|
})
|
|
218
|
-
|
|
225
|
+
// A module's top-level code runs as soon as it is imported, before any capability check can refuse
|
|
226
|
+
// it, so a cloned repository's extensions load only when the user asks.
|
|
227
|
+
const extensionRoots = options.extensions ? await discoverExtensions(options.cwd) : []
|
|
219
228
|
const extensions = new ExtensionHost({
|
|
220
229
|
cwd: options.cwd,
|
|
221
230
|
roots: extensionRoots,
|
package/src/context.ts
CHANGED
|
@@ -72,10 +72,41 @@ function safeEnv(
|
|
|
72
72
|
"LC_ALL",
|
|
73
73
|
"TERM",
|
|
74
74
|
"CI",
|
|
75
|
+
...(process.platform === "win32"
|
|
76
|
+
? [
|
|
77
|
+
"SystemRoot",
|
|
78
|
+
"WINDIR",
|
|
79
|
+
"TEMP",
|
|
80
|
+
"TMP",
|
|
81
|
+
"USERPROFILE",
|
|
82
|
+
"LOCALAPPDATA",
|
|
83
|
+
"APPDATA",
|
|
84
|
+
"COMSPEC",
|
|
85
|
+
"PATHEXT",
|
|
86
|
+
"BUN_INSTALL",
|
|
87
|
+
"HOMEDRIVE",
|
|
88
|
+
"HOMEPATH",
|
|
89
|
+
"USERNAME",
|
|
90
|
+
"USERDOMAIN",
|
|
91
|
+
"ProgramData",
|
|
92
|
+
"ProgramFiles",
|
|
93
|
+
"ProgramFiles(x86)",
|
|
94
|
+
]
|
|
95
|
+
: []),
|
|
75
96
|
...Object.keys(overrides ?? {}),
|
|
76
97
|
])) {
|
|
77
|
-
const value =
|
|
98
|
+
const value =
|
|
99
|
+
overrides !== undefined && Object.hasOwn(overrides, name) ? overrides[name] : readEnv(name)
|
|
78
100
|
if (value !== undefined) result[name] = value
|
|
79
101
|
}
|
|
80
102
|
return result
|
|
81
103
|
}
|
|
104
|
+
|
|
105
|
+
function readEnv(name: string): string | undefined {
|
|
106
|
+
const exact = process.env[name]
|
|
107
|
+
if (exact !== undefined || process.platform !== "win32") return exact
|
|
108
|
+
const key = Object.keys(process.env).find(
|
|
109
|
+
(candidate) => candidate.toLowerCase() === name.toLowerCase(),
|
|
110
|
+
)
|
|
111
|
+
return key === undefined ? undefined : process.env[key]
|
|
112
|
+
}
|
package/src/diff.ts
CHANGED
|
@@ -72,9 +72,47 @@ export async function readProjectDiff(options: ProjectDiffOptions): Promise<Proj
|
|
|
72
72
|
|
|
73
73
|
function safeEnv(): Record<string, string> {
|
|
74
74
|
const result: Record<string, string> = {}
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const names = [
|
|
76
|
+
"PATH",
|
|
77
|
+
"HOME",
|
|
78
|
+
"LANG",
|
|
79
|
+
"LC_ALL",
|
|
80
|
+
"TERM",
|
|
81
|
+
"GIT_PAGER",
|
|
82
|
+
...(process.platform === "win32"
|
|
83
|
+
? [
|
|
84
|
+
"SystemRoot",
|
|
85
|
+
"WINDIR",
|
|
86
|
+
"TEMP",
|
|
87
|
+
"TMP",
|
|
88
|
+
"USERPROFILE",
|
|
89
|
+
"LOCALAPPDATA",
|
|
90
|
+
"APPDATA",
|
|
91
|
+
"COMSPEC",
|
|
92
|
+
"PATHEXT",
|
|
93
|
+
"BUN_INSTALL",
|
|
94
|
+
"HOMEDRIVE",
|
|
95
|
+
"HOMEPATH",
|
|
96
|
+
"USERNAME",
|
|
97
|
+
"USERDOMAIN",
|
|
98
|
+
"ProgramData",
|
|
99
|
+
"ProgramFiles",
|
|
100
|
+
"ProgramFiles(x86)",
|
|
101
|
+
]
|
|
102
|
+
: []),
|
|
103
|
+
]
|
|
104
|
+
for (const name of names) {
|
|
105
|
+
const value = name === "GIT_PAGER" ? "cat" : readEnv(name)
|
|
77
106
|
if (value !== undefined) result[name] = value
|
|
78
107
|
}
|
|
79
108
|
return result
|
|
80
109
|
}
|
|
110
|
+
|
|
111
|
+
function readEnv(name: string): string | undefined {
|
|
112
|
+
const exact = process.env[name]
|
|
113
|
+
if (exact !== undefined || process.platform !== "win32") return exact
|
|
114
|
+
const key = Object.keys(process.env).find(
|
|
115
|
+
(candidate) => candidate.toLowerCase() === name.toLowerCase(),
|
|
116
|
+
)
|
|
117
|
+
return key === undefined ? undefined : process.env[key]
|
|
118
|
+
}
|
package/src/handoffs.ts
CHANGED
|
@@ -27,6 +27,8 @@ import type { ApprovalManager } from "./approvals.ts"
|
|
|
27
27
|
import { ChildVectorTracker } from "./orchestration/policy.ts"
|
|
28
28
|
|
|
29
29
|
const ID_TOKEN = /^[A-Za-z0-9._:-]{1,128}$/
|
|
30
|
+
/** The agent protocol's token alphabet, which the approval broker accepts for session ids. */
|
|
31
|
+
const SESSION_TOKEN = /^[A-Za-z0-9._:/-]{1,128}$/
|
|
30
32
|
|
|
31
33
|
/** Stable, content-free reasons the coordinator refuses a handoff request or decision. */
|
|
32
34
|
export type HandoffRejection =
|
|
@@ -136,7 +138,8 @@ export class HandoffCoordinator {
|
|
|
136
138
|
!input.capability ||
|
|
137
139
|
input.capability.length > 128 ||
|
|
138
140
|
!input.from ||
|
|
139
|
-
input.from.length > 128
|
|
141
|
+
input.from.length > 128 ||
|
|
142
|
+
(input.sessionId !== undefined && !SESSION_TOKEN.test(input.sessionId))
|
|
140
143
|
)
|
|
141
144
|
throw new HandoffError("invalid_handoff")
|
|
142
145
|
if (this.records.has(input.requestId)) throw new HandoffError("duplicate")
|
|
@@ -162,13 +165,18 @@ export class HandoffCoordinator {
|
|
|
162
165
|
}
|
|
163
166
|
this.records.set(input.requestId, record)
|
|
164
167
|
|
|
168
|
+
// The mirror carries the boundary's coordinate, so only a matched decision approves it. It is a
|
|
169
|
+
// notification: a broker that is full or fails to broadcast leaves the handoff itself intact.
|
|
165
170
|
if (record.requireApproval && this.approvals !== undefined)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
this.approvals
|
|
172
|
+
.offer({
|
|
173
|
+
id: input.requestId,
|
|
174
|
+
sessionId: input.sessionId ?? input.runId,
|
|
175
|
+
action: "handoff",
|
|
176
|
+
capability: input.capability,
|
|
177
|
+
coordinate,
|
|
178
|
+
})
|
|
179
|
+
.catch(() => {})
|
|
172
180
|
return this.view(record)
|
|
173
181
|
}
|
|
174
182
|
|
|
@@ -264,7 +272,11 @@ export class HandoffCoordinator {
|
|
|
264
272
|
|
|
265
273
|
private settleApproval(record: HandoffRecord, op: string): void {
|
|
266
274
|
if (!record.requireApproval || this.approvals === undefined) return
|
|
267
|
-
this.approvals.
|
|
275
|
+
this.approvals.resolveMatched(
|
|
276
|
+
record.coordinate,
|
|
277
|
+
op === "accept" || op === "resolve",
|
|
278
|
+
this.now(),
|
|
279
|
+
)
|
|
268
280
|
}
|
|
269
281
|
|
|
270
282
|
private isTerminal(state: HandoffLifecycleState): boolean {
|