intentdna 1.9.0 → 1.9.3
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +227 -87
- package/dist/cli/commands/run-lifecycle.d.ts +61 -11
- package/dist/cli/commands/run-lifecycle.js +184 -50
- package/dist/cli/commands/run-observer.d.ts +15 -0
- package/dist/cli/commands/run-observer.js +210 -0
- package/dist/cli/commands/run.d.ts +16 -20
- package/dist/cli/commands/run.js +461 -659
- package/dist/cli/commands/sync.js +16 -12
- package/dist/cli/index.js +14 -5
- package/dist/compiler/compile.d.ts +1 -0
- package/dist/compiler/compile.js +1 -1
- package/dist/compiler/controller.d.ts +16 -0
- package/dist/compiler/controller.js +693 -0
- package/dist/compiler/input-resolver.d.ts +2 -0
- package/dist/compiler/input-resolver.js +22 -6
- package/dist/hooks/cli.d.ts +45 -0
- package/dist/hooks/cli.js +263 -31
- package/dist/hooks/host-event.d.ts +57 -0
- package/dist/hooks/host-event.js +187 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/protocol.d.ts +7 -0
- package/dist/hooks/schema.d.ts +1 -0
- package/dist/hooks/schema.js +89 -1
- package/dist/hooks/state.d.ts +23 -1
- package/dist/hooks/state.js +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tools-run.d.ts +4 -0
- package/dist/mcp/tools-run.js +419 -0
- package/dist/mcp/tools-state.d.ts +1 -1
- package/dist/mcp/tools-state.js +13 -241
- package/dist/runtime/artifact-store.d.ts +146 -0
- package/dist/runtime/artifact-store.js +1436 -0
- package/dist/runtime/canonical-attempt-outcome.d.ts +12 -0
- package/dist/runtime/canonical-attempt-outcome.js +195 -0
- package/dist/runtime/canonical-json.d.ts +10 -0
- package/dist/runtime/canonical-json.js +105 -0
- package/dist/runtime/canonical-run-application.d.ts +93 -0
- package/dist/runtime/canonical-run-application.js +229 -0
- package/dist/runtime/canonical-run-service.d.ts +454 -0
- package/dist/runtime/canonical-run-service.js +4760 -0
- package/dist/runtime/canonical-runtime-composition.d.ts +22 -0
- package/dist/runtime/canonical-runtime-composition.js +34 -0
- package/dist/runtime/canonical-runtime-projection.d.ts +3 -0
- package/dist/runtime/canonical-runtime-projection.js +4 -0
- package/dist/runtime/canonical-target-compiler.d.ts +46 -0
- package/dist/runtime/canonical-target-compiler.js +514 -0
- package/dist/runtime/claude-agent-identity.d.ts +2 -0
- package/dist/runtime/claude-agent-identity.js +21 -0
- package/dist/runtime/executable-run-plan.d.ts +328 -0
- package/dist/runtime/executable-run-plan.js +1485 -0
- package/dist/runtime/execution-authority.d.ts +83 -0
- package/dist/runtime/execution-authority.js +98 -0
- package/dist/runtime/handoff-resolver.js +4 -1
- package/dist/runtime/harness-pull-adapter.d.ts +146 -0
- package/dist/runtime/harness-pull-adapter.js +315 -0
- package/dist/runtime/index.d.ts +39 -14
- package/dist/runtime/index.js +29 -7
- package/dist/runtime/local-execution-authority.d.ts +194 -0
- package/dist/runtime/local-execution-authority.js +2897 -0
- package/dist/runtime/local-execution-reconciliation.d.ts +20 -0
- package/dist/runtime/local-execution-reconciliation.js +107 -0
- package/dist/runtime/local-execution-supervisor-script.d.ts +7 -0
- package/dist/runtime/local-execution-supervisor-script.js +793 -0
- package/dist/runtime/local-provider-sandbox.d.ts +42 -0
- package/dist/runtime/local-provider-sandbox.js +154 -0
- package/dist/runtime/local-verifier-execution.d.ts +24 -0
- package/dist/runtime/local-verifier-execution.js +290 -0
- package/dist/runtime/local-windows-execution-supervisor-script.d.ts +6 -0
- package/dist/runtime/local-windows-execution-supervisor-script.js +788 -0
- package/dist/runtime/plan-store.d.ts +30 -0
- package/dist/runtime/plan-store.js +231 -0
- package/dist/runtime/process-tree.d.ts +9 -0
- package/dist/runtime/process-tree.js +97 -24
- package/dist/runtime/providers/codex.js +2 -2
- package/dist/runtime/push-driver.d.ts +184 -0
- package/dist/runtime/push-driver.js +1002 -0
- package/dist/runtime/result-store.d.ts +34 -3
- package/dist/runtime/result-store.js +1073 -2
- package/dist/runtime/run-binding.d.ts +68 -0
- package/dist/runtime/run-binding.js +278 -0
- package/dist/runtime/run-contracts.d.ts +575 -0
- package/dist/runtime/run-contracts.js +58 -7
- package/dist/runtime/run-controller.d.ts +1 -6
- package/dist/runtime/run-controller.js +0 -41
- package/dist/runtime/run-store.d.ts +85 -7
- package/dist/runtime/run-store.js +2528 -186
- package/dist/runtime/skill-adapter.d.ts +18 -7
- package/dist/runtime/skill-adapter.js +100 -742
- package/dist/runtime/structured-output-validator.d.ts +44 -0
- package/dist/runtime/structured-output-validator.js +171 -0
- package/dist/runtime/verifier-command-binding.d.ts +22 -0
- package/dist/runtime/verifier-command-binding.js +162 -0
- package/dist/runtime/verifier.d.ts +46 -6
- package/dist/runtime/verifier.js +355 -53
- package/dist/runtime/windows-job-keeper.d.ts +47 -0
- package/dist/runtime/windows-job-keeper.js +231 -0
- package/dist/runtime/worker-executor.d.ts +17 -1
- package/dist/runtime/worker-executor.js +26 -5
- package/dist/runtime/workflow-plan-adapter.d.ts +19 -0
- package/dist/runtime/workflow-plan-adapter.js +502 -6
- package/dist/runtime/workflow-runtime-manifest.d.ts +1 -0
- package/dist/runtime/workflow-runtime-manifest.js +5 -0
- package/dist/runtime/workspace-isolation.d.ts +29 -2
- package/dist/runtime/workspace-isolation.js +488 -11
- package/dist/runtime/workspace-observation.d.ts +44 -0
- package/dist/runtime/workspace-observation.js +216 -0
- package/dist/schema/controller-registry.d.ts +0 -7
- package/dist/schema/controller-registry.js +0 -8
- package/dist/schema/types.d.ts +84 -10
- package/dist/schema/types.js +2 -0
- package/dist/schema/validate.js +151 -1
- package/dist/schema/validators/controllers.d.ts +1 -1
- package/dist/schema/validators/controllers.js +126 -41
- package/dist/schema/workflow-authoring-contract.js +2 -0
- package/dist/schema/yaml-parser.js +1 -1
- package/dist/templates/flutter-rewrite-new.dna.yaml +1 -0
- package/dist/templates/flutter-rewrite.dna.yaml +87 -60
- package/dist/templates/safe-refactoring.dna.yaml +1 -0
- package/native/windows-job-keeper/README.md +80 -0
- package/native/windows-job-keeper/bin/aarch64/intentdna-windows-job-keeper.exe +0 -0
- package/native/windows-job-keeper/bin/x86_64/intentdna-windows-job-keeper.exe +0 -0
- package/native/windows-job-keeper/keeper.c +979 -0
- package/native/windows-job-keeper/manifest.json +33 -0
- package/package.json +6 -2
- package/scripts/build-windows-job-keeper.mjs +172 -0
- package/scripts/verify-windows-job-keeper.mjs +184 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ExecutableRunPlanPayload, type Sha256Digest, type StoredExecutableRunPlan } from "./executable-run-plan.js";
|
|
2
|
+
export interface DurablePlanStoreOptions {
|
|
3
|
+
/** The shared `.dna/runtime` directory. */
|
|
4
|
+
readonly root_directory: string;
|
|
5
|
+
}
|
|
6
|
+
export type PlanStoreErrorCode = "invalid_options" | "invalid_plan_id" | "invalid_plan" | "plan_not_found" | "corrupt_plan" | "immutable_conflict";
|
|
7
|
+
export declare class PlanStoreError extends Error {
|
|
8
|
+
readonly code: PlanStoreErrorCode;
|
|
9
|
+
constructor(code: PlanStoreErrorCode, message: string, options?: ErrorOptions);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Durable immutable storage for canonical executable plans.
|
|
13
|
+
*
|
|
14
|
+
* A plan is first written and synced under a private temporary name. Linking
|
|
15
|
+
* that complete inode to its digest path is the publication CAS: link(2) is
|
|
16
|
+
* atomic and never replaces an existing destination.
|
|
17
|
+
*/
|
|
18
|
+
export declare class DurablePlanStore {
|
|
19
|
+
private readonly rootDirectory;
|
|
20
|
+
constructor(options: DurablePlanStoreOptions);
|
|
21
|
+
private plansDirectory;
|
|
22
|
+
private planPath;
|
|
23
|
+
private readStoredPlan;
|
|
24
|
+
commitPlan(plan: ExecutableRunPlanPayload): Promise<StoredExecutableRunPlan>;
|
|
25
|
+
loadPlan(planId: string): Promise<StoredExecutableRunPlan | null>;
|
|
26
|
+
requirePlan(planId: string): Promise<StoredExecutableRunPlan>;
|
|
27
|
+
hasPlan(planId: string): Promise<boolean>;
|
|
28
|
+
listPlanIds(): Promise<readonly Sha256Digest[]>;
|
|
29
|
+
loadAllPlans(): Promise<readonly StoredExecutableRunPlan[]>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, link, lstat, mkdir, open, readFile, readdir, unlink, } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { canonicalizeJson } from "./canonical-json.js";
|
|
5
|
+
import { createStoredExecutableRunPlan, parseStoredExecutableRunPlan, } from "./executable-run-plan.js";
|
|
6
|
+
export class PlanStoreError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message, options) {
|
|
9
|
+
super(message, options);
|
|
10
|
+
this.name = "PlanStoreError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const DIRECTORY_MODE = 0o700;
|
|
15
|
+
const TEMPORARY_FILE_MODE = 0o600;
|
|
16
|
+
const STORED_FILE_MODE = 0o400;
|
|
17
|
+
const PLAN_FILE_PATTERN = /^([a-f0-9]{64})\.json$/u;
|
|
18
|
+
const PLAN_ID_PATTERN = /^sha256:([a-f0-9]{64})$/u;
|
|
19
|
+
function isErrno(error, code) {
|
|
20
|
+
return (error instanceof Error
|
|
21
|
+
&& "code" in error
|
|
22
|
+
&& error.code === code);
|
|
23
|
+
}
|
|
24
|
+
function assertPlanId(planId) {
|
|
25
|
+
if (!PLAN_ID_PATTERN.test(planId)) {
|
|
26
|
+
throw new PlanStoreError("invalid_plan_id", "plan_id must be a lowercase sha256 digest");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function planFileName(planId) {
|
|
30
|
+
return `${planId.slice("sha256:".length)}.json`;
|
|
31
|
+
}
|
|
32
|
+
function isUnsupportedDirectorySync(error) {
|
|
33
|
+
if (!(error instanceof Error) || !("code" in error))
|
|
34
|
+
return false;
|
|
35
|
+
const code = error.code;
|
|
36
|
+
return (code === "EINVAL"
|
|
37
|
+
|| code === "ENOTSUP"
|
|
38
|
+
|| code === "EISDIR"
|
|
39
|
+
|| (code === "EPERM" && process.platform === "win32"));
|
|
40
|
+
}
|
|
41
|
+
async function syncDirectory(path) {
|
|
42
|
+
let handle;
|
|
43
|
+
try {
|
|
44
|
+
handle = await open(path, "r");
|
|
45
|
+
await handle.sync();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (!isUnsupportedDirectorySync(error))
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await handle?.close().catch(() => undefined);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function ensurePrivateDirectory(path) {
|
|
56
|
+
await mkdir(path, { recursive: true, mode: DIRECTORY_MODE });
|
|
57
|
+
const metadata = await lstat(path);
|
|
58
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
59
|
+
throw new PlanStoreError("immutable_conflict", `plan store path is not a real directory: ${path}`);
|
|
60
|
+
}
|
|
61
|
+
await chmod(path, DIRECTORY_MODE);
|
|
62
|
+
}
|
|
63
|
+
async function requireRealDirectoryIfPresent(path) {
|
|
64
|
+
let metadata;
|
|
65
|
+
try {
|
|
66
|
+
metadata = await lstat(path);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (isErrno(error, "ENOENT"))
|
|
70
|
+
return false;
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
74
|
+
throw new PlanStoreError("immutable_conflict", `plan store path is not a real directory: ${path}`);
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Durable immutable storage for canonical executable plans.
|
|
80
|
+
*
|
|
81
|
+
* A plan is first written and synced under a private temporary name. Linking
|
|
82
|
+
* that complete inode to its digest path is the publication CAS: link(2) is
|
|
83
|
+
* atomic and never replaces an existing destination.
|
|
84
|
+
*/
|
|
85
|
+
export class DurablePlanStore {
|
|
86
|
+
rootDirectory;
|
|
87
|
+
constructor(options) {
|
|
88
|
+
if (!options.root_directory.trim()) {
|
|
89
|
+
throw new PlanStoreError("invalid_options", "root_directory must not be empty");
|
|
90
|
+
}
|
|
91
|
+
this.rootDirectory = resolve(options.root_directory);
|
|
92
|
+
}
|
|
93
|
+
plansDirectory() {
|
|
94
|
+
return join(this.rootDirectory, "plans");
|
|
95
|
+
}
|
|
96
|
+
planPath(planId) {
|
|
97
|
+
return join(this.plansDirectory(), planFileName(planId));
|
|
98
|
+
}
|
|
99
|
+
async readStoredPlan(planId) {
|
|
100
|
+
if (!await requireRealDirectoryIfPresent(this.rootDirectory))
|
|
101
|
+
return null;
|
|
102
|
+
if (!await requireRealDirectoryIfPresent(this.plansDirectory()))
|
|
103
|
+
return null;
|
|
104
|
+
const path = this.planPath(planId);
|
|
105
|
+
let metadata;
|
|
106
|
+
try {
|
|
107
|
+
metadata = await lstat(path);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (isErrno(error, "ENOENT"))
|
|
111
|
+
return null;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
115
|
+
throw new PlanStoreError("corrupt_plan", `stored plan ${planId} is not a regular file`);
|
|
116
|
+
}
|
|
117
|
+
if ((metadata.mode & 0o222) !== 0) {
|
|
118
|
+
throw new PlanStoreError("corrupt_plan", `stored plan ${planId} must be read-only`);
|
|
119
|
+
}
|
|
120
|
+
let value;
|
|
121
|
+
try {
|
|
122
|
+
value = JSON.parse(await readFile(path, "utf8"));
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (isErrno(error, "ENOENT"))
|
|
126
|
+
return null;
|
|
127
|
+
if (error instanceof SyntaxError) {
|
|
128
|
+
throw new PlanStoreError("corrupt_plan", `stored plan ${planId} contains invalid JSON`, { cause: error });
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
let stored;
|
|
133
|
+
try {
|
|
134
|
+
stored = parseStoredExecutableRunPlan(value);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
throw new PlanStoreError("corrupt_plan", `stored plan ${planId} failed schema or digest validation`, { cause: error });
|
|
138
|
+
}
|
|
139
|
+
if (stored.plan_id !== planId) {
|
|
140
|
+
throw new PlanStoreError("corrupt_plan", `stored plan ${stored.plan_id} does not match requested identity ${planId}`);
|
|
141
|
+
}
|
|
142
|
+
return stored;
|
|
143
|
+
}
|
|
144
|
+
async commitPlan(plan) {
|
|
145
|
+
let stored;
|
|
146
|
+
try {
|
|
147
|
+
stored = createStoredExecutableRunPlan(plan);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw new PlanStoreError("invalid_plan", "executable run plan failed schema validation", { cause: error });
|
|
151
|
+
}
|
|
152
|
+
const plansDirectory = this.plansDirectory();
|
|
153
|
+
await ensurePrivateDirectory(this.rootDirectory);
|
|
154
|
+
await ensurePrivateDirectory(plansDirectory);
|
|
155
|
+
const destination = this.planPath(stored.plan_id);
|
|
156
|
+
const temporaryPath = join(plansDirectory, `.${planFileName(stored.plan_id)}.tmp.${process.pid}.${randomUUID()}`);
|
|
157
|
+
let handle;
|
|
158
|
+
try {
|
|
159
|
+
handle = await open(temporaryPath, "wx", TEMPORARY_FILE_MODE);
|
|
160
|
+
await handle.writeFile(`${canonicalizeJson(stored)}\n`, "utf8");
|
|
161
|
+
await handle.chmod(STORED_FILE_MODE);
|
|
162
|
+
await handle.sync();
|
|
163
|
+
await handle.close();
|
|
164
|
+
handle = undefined;
|
|
165
|
+
while (true) {
|
|
166
|
+
try {
|
|
167
|
+
await link(temporaryPath, destination);
|
|
168
|
+
await syncDirectory(plansDirectory);
|
|
169
|
+
return stored;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (!isErrno(error, "EEXIST"))
|
|
173
|
+
throw error;
|
|
174
|
+
await syncDirectory(plansDirectory);
|
|
175
|
+
const existing = await this.readStoredPlan(stored.plan_id);
|
|
176
|
+
if (existing)
|
|
177
|
+
return existing;
|
|
178
|
+
// A concurrent external deletion raced the EEXIST check; retry the CAS.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await handle?.close().catch(() => undefined);
|
|
184
|
+
await unlink(temporaryPath).catch((error) => {
|
|
185
|
+
if (!isErrno(error, "ENOENT"))
|
|
186
|
+
throw error;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async loadPlan(planId) {
|
|
191
|
+
assertPlanId(planId);
|
|
192
|
+
return this.readStoredPlan(planId);
|
|
193
|
+
}
|
|
194
|
+
async requirePlan(planId) {
|
|
195
|
+
const stored = await this.loadPlan(planId);
|
|
196
|
+
if (!stored) {
|
|
197
|
+
throw new PlanStoreError("plan_not_found", `stored plan ${planId} was not found`);
|
|
198
|
+
}
|
|
199
|
+
return stored;
|
|
200
|
+
}
|
|
201
|
+
async hasPlan(planId) {
|
|
202
|
+
return (await this.loadPlan(planId)) !== null;
|
|
203
|
+
}
|
|
204
|
+
async listPlanIds() {
|
|
205
|
+
if (!await requireRealDirectoryIfPresent(this.rootDirectory))
|
|
206
|
+
return [];
|
|
207
|
+
if (!await requireRealDirectoryIfPresent(this.plansDirectory()))
|
|
208
|
+
return [];
|
|
209
|
+
let entries;
|
|
210
|
+
try {
|
|
211
|
+
entries = await readdir(this.plansDirectory(), { withFileTypes: true });
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (isErrno(error, "ENOENT"))
|
|
215
|
+
return [];
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
const ids = entries
|
|
219
|
+
.map((entry) => entry.name.match(PLAN_FILE_PATTERN)?.[1])
|
|
220
|
+
.filter((digest) => digest !== undefined)
|
|
221
|
+
.sort()
|
|
222
|
+
.map((digest) => `sha256:${digest}`);
|
|
223
|
+
for (const planId of ids) {
|
|
224
|
+
await this.requirePlan(planId);
|
|
225
|
+
}
|
|
226
|
+
return ids;
|
|
227
|
+
}
|
|
228
|
+
async loadAllPlans() {
|
|
229
|
+
return Promise.all((await this.listPlanIds()).map((planId) => this.requirePlan(planId)));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -41,7 +41,16 @@ export interface ProcessTreeRunResult {
|
|
|
41
41
|
readonly processLimitExceeded: boolean;
|
|
42
42
|
readonly outputLimitExceeded: boolean;
|
|
43
43
|
readonly terminationReason: ProcessTerminationReason | null;
|
|
44
|
+
readonly processId: number | null;
|
|
45
|
+
readonly processTreeDrained: boolean;
|
|
44
46
|
readonly error?: NodeJS.ErrnoException;
|
|
45
47
|
}
|
|
48
|
+
export interface ProcessCommandSpec {
|
|
49
|
+
readonly command: string;
|
|
50
|
+
readonly args: readonly string[];
|
|
51
|
+
readonly windowsVerbatimArguments?: boolean;
|
|
52
|
+
readonly windowsBatchTransport?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export declare function resolveProcessCommand(command: string, args: readonly string[], platform: NodeJS.Platform, env: NodeJS.ProcessEnv, cwd?: string): ProcessCommandSpec;
|
|
46
55
|
export declare function runProcessTree(command: string, args: readonly string[], options?: ProcessTreeRunOptions): Promise<ProcessTreeRunResult>;
|
|
47
56
|
export declare const runProcessTreeWithTimeout: typeof runProcessTree;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { spawn, spawnSync, } from "node:child_process";
|
|
10
10
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
11
|
-
import {
|
|
11
|
+
import { win32 as win32Path, } from "node:path";
|
|
12
12
|
const DEFAULT_TERMINATION_GRACE_MS = 1_000;
|
|
13
13
|
const DEFAULT_PROCESS_LIMIT_POLL_MS = 100;
|
|
14
14
|
const WINDOWS_DIRECT_EXTENSIONS = new Set([".com", ".exe"]);
|
|
@@ -42,8 +42,8 @@ function fileExists(path) {
|
|
|
42
42
|
return false;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
function resolveWindowsCommand(command, env) {
|
|
46
|
-
const hasPath =
|
|
45
|
+
function resolveWindowsCommand(command, env, cwd) {
|
|
46
|
+
const hasPath = win32Path.isAbsolute(command) || /[\\/]/.test(command);
|
|
47
47
|
const rawExtensions = String(env.PATHEXT ?? "").trim();
|
|
48
48
|
const extensions = rawExtensions === ""
|
|
49
49
|
? WINDOWS_DEFAULT_PATHEXT
|
|
@@ -53,10 +53,12 @@ function resolveWindowsCommand(command, env) {
|
|
|
53
53
|
])];
|
|
54
54
|
const directories = hasPath
|
|
55
55
|
? [""]
|
|
56
|
-
: String(env.Path ?? env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
57
|
-
const extension = extname(command);
|
|
56
|
+
: [cwd, ...String(env.Path ?? env.PATH ?? "").split(win32Path.delimiter).filter(Boolean)];
|
|
57
|
+
const extension = win32Path.extname(command);
|
|
58
58
|
for (const directory of directories) {
|
|
59
|
-
const base =
|
|
59
|
+
const base = hasPath
|
|
60
|
+
? win32Path.isAbsolute(command) ? command : win32Path.resolve(cwd, command)
|
|
61
|
+
: win32Path.join(directory, command);
|
|
60
62
|
const candidates = extension ? [base] : [...extensions.map((item) => `${base}${item}`), base];
|
|
61
63
|
for (const candidate of candidates) {
|
|
62
64
|
if (fileExists(candidate))
|
|
@@ -65,49 +67,63 @@ function resolveWindowsCommand(command, env) {
|
|
|
65
67
|
}
|
|
66
68
|
return null;
|
|
67
69
|
}
|
|
68
|
-
function
|
|
70
|
+
export function resolveProcessCommand(command, args, platform, env, cwd = process.cwd()) {
|
|
69
71
|
if (platform !== "win32")
|
|
70
72
|
return { command, args: [...args] };
|
|
71
|
-
const resolved = resolveWindowsCommand(command, env);
|
|
73
|
+
const resolved = resolveWindowsCommand(command, env, cwd);
|
|
72
74
|
if (!resolved)
|
|
73
75
|
return { command, args: [...args] };
|
|
74
|
-
const extension = extname(resolved).toLowerCase();
|
|
76
|
+
const extension = win32Path.extname(resolved).toLowerCase();
|
|
75
77
|
if (WINDOWS_CMD_EXTENSIONS.has(extension)) {
|
|
76
|
-
const quote = (value) =>
|
|
78
|
+
const quote = (value) => {
|
|
79
|
+
const escapedQuotes = value.replace(/"/g, "\"\"");
|
|
80
|
+
const escapedTrailingBackslashes = escapedQuotes.replace(/(\\+)$/, "$1$1");
|
|
81
|
+
return `"${escapedTrailingBackslashes}"`;
|
|
82
|
+
};
|
|
77
83
|
const commandLine = [resolved, ...args].map(quote).join(" ");
|
|
78
84
|
return {
|
|
79
85
|
command: env.ComSpec || "cmd.exe",
|
|
80
86
|
args: ["/d", "/s", "/c", `"${commandLine}"`],
|
|
81
87
|
windowsVerbatimArguments: true,
|
|
88
|
+
windowsBatchTransport: true,
|
|
82
89
|
};
|
|
83
90
|
}
|
|
84
91
|
if (extension === ".ps1") {
|
|
92
|
+
const escapedScript = resolved.replace(/'/g, "''");
|
|
93
|
+
const encodedArgs = Buffer.from(JSON.stringify({ args }), "utf8").toString("base64");
|
|
94
|
+
const script = [
|
|
95
|
+
"$PSNativeCommandArgumentPassing = 'Standard'",
|
|
96
|
+
`$intentdnaDoc = ConvertFrom-Json -InputObject ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedArgs}')))`,
|
|
97
|
+
"[string[]] $intentdnaArgs = [string[]]::new($intentdnaDoc.args.Count)",
|
|
98
|
+
"for ($intentdnaIndex = 0; $intentdnaIndex -lt $intentdnaDoc.args.Count; $intentdnaIndex++) { $intentdnaArgs[$intentdnaIndex] = [string] $intentdnaDoc.args[$intentdnaIndex] }",
|
|
99
|
+
`& '${escapedScript}' @intentdnaArgs`,
|
|
100
|
+
"exit $LASTEXITCODE",
|
|
101
|
+
].join("; ");
|
|
85
102
|
return {
|
|
86
|
-
command: "
|
|
103
|
+
command: "pwsh.exe",
|
|
87
104
|
args: [
|
|
88
105
|
"-NoLogo",
|
|
89
106
|
"-NoProfile",
|
|
90
107
|
"-ExecutionPolicy",
|
|
91
108
|
"Bypass",
|
|
92
|
-
"-
|
|
93
|
-
|
|
94
|
-
...args,
|
|
109
|
+
"-EncodedCommand",
|
|
110
|
+
Buffer.from(script, "utf16le").toString("base64"),
|
|
95
111
|
],
|
|
96
112
|
};
|
|
97
113
|
}
|
|
98
|
-
if (WINDOWS_DIRECT_EXTENSIONS.has(extension) || basename(resolved) !== "") {
|
|
114
|
+
if (WINDOWS_DIRECT_EXTENSIONS.has(extension) || win32Path.basename(resolved) !== "") {
|
|
99
115
|
return { command: resolved, args: [...args] };
|
|
100
116
|
}
|
|
101
117
|
return { command, args: [...args] };
|
|
102
118
|
}
|
|
103
119
|
function terminateProcessTree(child, platform, signal) {
|
|
104
120
|
if (child.pid === undefined)
|
|
105
|
-
return;
|
|
121
|
+
return true;
|
|
106
122
|
if (platform === "win32") {
|
|
107
123
|
const force = signal === "SIGKILL" ? ["/F"] : [];
|
|
108
124
|
const result = spawnSync("taskkill", ["/PID", String(child.pid), "/T", ...force], { windowsHide: true, stdio: "ignore" });
|
|
109
125
|
if (!result.error && result.status === 0)
|
|
110
|
-
return;
|
|
126
|
+
return true;
|
|
111
127
|
}
|
|
112
128
|
try {
|
|
113
129
|
if (platform === "win32") {
|
|
@@ -120,7 +136,7 @@ function terminateProcessTree(child, platform, signal) {
|
|
|
120
136
|
catch (error) {
|
|
121
137
|
const code = error.code;
|
|
122
138
|
if (code === "ESRCH")
|
|
123
|
-
return;
|
|
139
|
+
return platform !== "win32";
|
|
124
140
|
try {
|
|
125
141
|
child.kill(signal);
|
|
126
142
|
}
|
|
@@ -128,8 +144,52 @@ function terminateProcessTree(child, platform, signal) {
|
|
|
128
144
|
if (fallbackError.code !== "ESRCH") {
|
|
129
145
|
throw fallbackError;
|
|
130
146
|
}
|
|
147
|
+
return platform !== "win32";
|
|
131
148
|
}
|
|
132
149
|
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
function processTreeExists(processId, platform) {
|
|
153
|
+
if (platform === "win32")
|
|
154
|
+
return null;
|
|
155
|
+
try {
|
|
156
|
+
process.kill(-processId, 0);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const code = error.code;
|
|
161
|
+
if (code === "ESRCH")
|
|
162
|
+
return false;
|
|
163
|
+
if (code === "EPERM")
|
|
164
|
+
return true;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function drainProcessTree(child, platform, signal, graceMs) {
|
|
169
|
+
if (child.pid === undefined)
|
|
170
|
+
return true;
|
|
171
|
+
if (terminateProcessTree(child, platform, signal) && platform === "win32")
|
|
172
|
+
return true;
|
|
173
|
+
const deadline = Date.now() + graceMs;
|
|
174
|
+
for (;;) {
|
|
175
|
+
const exists = processTreeExists(child.pid, platform);
|
|
176
|
+
if (exists === false)
|
|
177
|
+
return true;
|
|
178
|
+
if (exists === null || Date.now() >= deadline)
|
|
179
|
+
break;
|
|
180
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
181
|
+
}
|
|
182
|
+
if (terminateProcessTree(child, platform, "SIGKILL") && platform === "win32")
|
|
183
|
+
return true;
|
|
184
|
+
const forceDeadline = Date.now() + Math.max(100, graceMs);
|
|
185
|
+
for (;;) {
|
|
186
|
+
const exists = processTreeExists(child.pid, platform);
|
|
187
|
+
if (exists === false)
|
|
188
|
+
return true;
|
|
189
|
+
if (exists === null || Date.now() >= forceDeadline)
|
|
190
|
+
return false;
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
192
|
+
}
|
|
133
193
|
}
|
|
134
194
|
function readLinuxProcessTable() {
|
|
135
195
|
try {
|
|
@@ -195,11 +255,13 @@ export function runProcessTree(command, args, options = {}) {
|
|
|
195
255
|
processLimitExceeded: false,
|
|
196
256
|
outputLimitExceeded: false,
|
|
197
257
|
terminationReason: "cancellation",
|
|
258
|
+
processId: null,
|
|
259
|
+
processTreeDrained: true,
|
|
198
260
|
});
|
|
199
261
|
}
|
|
200
262
|
const platform = options.platform ?? process.platform;
|
|
201
263
|
const env = options.env ?? process.env;
|
|
202
|
-
const spec =
|
|
264
|
+
const spec = resolveProcessCommand(command, args, platform, env, options.cwd ?? process.cwd());
|
|
203
265
|
const spawnImpl = options.spawnImpl ?? spawn;
|
|
204
266
|
const timeoutMs = positiveInteger(options.timeoutMs);
|
|
205
267
|
const killSignal = options.killSignal ?? "SIGTERM";
|
|
@@ -223,6 +285,7 @@ export function runProcessTree(command, args, options = {}) {
|
|
|
223
285
|
let settled = false;
|
|
224
286
|
let timeoutTimer;
|
|
225
287
|
let forceKillTimer;
|
|
288
|
+
let residualKillTimer;
|
|
226
289
|
let processLimitTimer;
|
|
227
290
|
const spawnOptions = {
|
|
228
291
|
cwd: options.cwd,
|
|
@@ -248,6 +311,8 @@ export function runProcessTree(command, args, options = {}) {
|
|
|
248
311
|
processLimitExceeded: false,
|
|
249
312
|
outputLimitExceeded: false,
|
|
250
313
|
terminationReason: null,
|
|
314
|
+
processId: null,
|
|
315
|
+
processTreeDrained: true,
|
|
251
316
|
error: error,
|
|
252
317
|
});
|
|
253
318
|
return;
|
|
@@ -294,7 +359,7 @@ export function runProcessTree(command, args, options = {}) {
|
|
|
294
359
|
requestTermination("cancellation", cancellationGraceMs);
|
|
295
360
|
};
|
|
296
361
|
cancellationSignal?.addEventListener("abort", cancellationHandler, { once: true });
|
|
297
|
-
const finish = (status, signal, error) => {
|
|
362
|
+
const finish = async (status, signal, error) => {
|
|
298
363
|
if (settled)
|
|
299
364
|
return;
|
|
300
365
|
settled = true;
|
|
@@ -302,10 +367,16 @@ export function runProcessTree(command, args, options = {}) {
|
|
|
302
367
|
clearTimeout(timeoutTimer);
|
|
303
368
|
if (forceKillTimer)
|
|
304
369
|
clearTimeout(forceKillTimer);
|
|
370
|
+
if (residualKillTimer)
|
|
371
|
+
clearTimeout(residualKillTimer);
|
|
305
372
|
if (processLimitTimer)
|
|
306
373
|
clearInterval(processLimitTimer);
|
|
307
374
|
cancellationSignal?.removeEventListener("abort", cancellationHandler);
|
|
308
375
|
removeParentHandlers();
|
|
376
|
+
const residualGraceMs = terminationReason === "cancellation"
|
|
377
|
+
? cancellationGraceMs
|
|
378
|
+
: timeoutGraceMs;
|
|
379
|
+
const processTreeDrained = await drainProcessTree(child, platform, killSignal, residualGraceMs);
|
|
309
380
|
resolve({
|
|
310
381
|
stdout,
|
|
311
382
|
stderr,
|
|
@@ -319,6 +390,8 @@ export function runProcessTree(command, args, options = {}) {
|
|
|
319
390
|
processLimitExceeded,
|
|
320
391
|
outputLimitExceeded,
|
|
321
392
|
terminationReason,
|
|
393
|
+
processId: child.pid ?? null,
|
|
394
|
+
processTreeDrained,
|
|
322
395
|
...(error ? { error } : {}),
|
|
323
396
|
});
|
|
324
397
|
};
|
|
@@ -356,7 +429,7 @@ export function runProcessTree(command, args, options = {}) {
|
|
|
356
429
|
});
|
|
357
430
|
child.stdin?.end(options.stdin);
|
|
358
431
|
child.on("error", (error) => {
|
|
359
|
-
finish(null, null, error);
|
|
432
|
+
void finish(null, null, error);
|
|
360
433
|
});
|
|
361
434
|
child.on("exit", () => {
|
|
362
435
|
if (platform === "win32")
|
|
@@ -365,13 +438,13 @@ export function runProcessTree(command, args, options = {}) {
|
|
|
365
438
|
const residualGraceMs = terminationReason === "cancellation"
|
|
366
439
|
? cancellationGraceMs
|
|
367
440
|
: timeoutGraceMs;
|
|
368
|
-
|
|
441
|
+
residualKillTimer = setTimeout(() => {
|
|
369
442
|
terminateProcessTree(child, platform, "SIGKILL");
|
|
370
443
|
}, residualGraceMs);
|
|
371
|
-
|
|
444
|
+
residualKillTimer.unref?.();
|
|
372
445
|
});
|
|
373
446
|
child.on("close", (status, signal) => {
|
|
374
|
-
finish(status, signal);
|
|
447
|
+
void finish(status, signal);
|
|
375
448
|
});
|
|
376
449
|
if (cancellationSignal?.aborted)
|
|
377
450
|
cancellationHandler();
|
|
@@ -18,9 +18,9 @@ export function createCodexExecutionProvider(options = {}) {
|
|
|
18
18
|
"--sandbox",
|
|
19
19
|
options.sandbox ?? "workspace-write",
|
|
20
20
|
...(options.extraArgs ?? []),
|
|
21
|
-
|
|
21
|
+
"-",
|
|
22
22
|
],
|
|
23
|
-
stdin:
|
|
23
|
+
stdin: packet.standalone_prompt,
|
|
24
24
|
cwd: packet.workspace.working_directory,
|
|
25
25
|
...(options.env ? { env: options.env } : {}),
|
|
26
26
|
};
|