@awak-app/simy-cli 0.2.1 → 0.2.2
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 +8 -0
- package/package.json +4 -3
- package/src/agent.js +135 -0
- package/src/cli-contract.js +1 -1
- package/src/desktop-executor.js +43 -2
- package/src/direct-executor.js +142 -0
- package/src/execution-guardrail.js +120 -0
package/README.md
CHANGED
|
@@ -36,6 +36,14 @@ runs. It builds the requirement charter, runs Codex or Claude Code, audits
|
|
|
36
36
|
structured completion evidence, and re-instructs the executor within the
|
|
37
37
|
configured attempt budget.
|
|
38
38
|
|
|
39
|
+
Before Web creates an Agentic Loop, it classifies whether the request actually
|
|
40
|
+
needs a managed PR, verification, evidence, and merge lifecycle. A bounded
|
|
41
|
+
ordinary task is sent to CLI 0.2.2 or newer through the one-time direct executor
|
|
42
|
+
instead. CLI independently recomputes the guardrail, restricts read-only work at
|
|
43
|
+
the Provider command boundary, and permits exactly one Provider invocation. It
|
|
44
|
+
does not create an Agentic Loop ledger, audit session, or retry lifecycle for
|
|
45
|
+
that task.
|
|
46
|
+
|
|
39
47
|
## CLI updates
|
|
40
48
|
|
|
41
49
|
The CLI checks the npm registry at startup and once per hour while it is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Local SIMY Agentic Loop executor for Codex and Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
"test:e2e:hypothesis-retries": "node ./scripts/hypothesis-retries-e2e.js",
|
|
26
26
|
"test:e2e:budgets": "node ./scripts/budget-enforcement-e2e.js",
|
|
27
27
|
"test:e2e:desktop-executor": "node ./scripts/desktop-executor-e2e.js",
|
|
28
|
+
"test:e2e:pre-guardrail": "node ./scripts/pre-guardrail-e2e.js",
|
|
28
29
|
"test:e2e:console:real-provider": "node ./scripts/real-provider-console-e2e.js",
|
|
29
30
|
"test:e2e:console:fullstack": "node ./scripts/fullstack-cli-smoke.js",
|
|
30
|
-
"check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
|
|
31
|
+
"check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/pre-guardrail-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
|
|
31
32
|
"check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js",
|
|
32
33
|
"check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js",
|
|
33
|
-
"check:syntax": "node --check ./src/index.js && node --check ./src/agent.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/run-registry.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
|
|
34
|
+
"check:syntax": "node --check ./src/index.js && node --check ./src/agent.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
|
|
34
35
|
"check:package": "node ./scripts/check-package-contents.js"
|
|
35
36
|
},
|
|
36
37
|
"engines": {
|
package/src/agent.js
CHANGED
|
@@ -70,6 +70,16 @@ import {
|
|
|
70
70
|
CLI_VERSION,
|
|
71
71
|
withCliContract,
|
|
72
72
|
} from "./cli-contract.js";
|
|
73
|
+
import {
|
|
74
|
+
createDirectTask,
|
|
75
|
+
directTaskSnapshot,
|
|
76
|
+
DirectTaskRegistry,
|
|
77
|
+
startDirectTask,
|
|
78
|
+
} from "./direct-executor.js";
|
|
79
|
+
import {
|
|
80
|
+
classifyExecutionRequirement,
|
|
81
|
+
EXECUTION_GUARDRAIL_POLICY_VERSION,
|
|
82
|
+
} from "./execution-guardrail.js";
|
|
73
83
|
|
|
74
84
|
const DEVICE_HEARTBEAT_INTERVAL_MS = 20_000;
|
|
75
85
|
|
|
@@ -84,6 +94,7 @@ export async function startAgent({
|
|
|
84
94
|
} = {}) {
|
|
85
95
|
const apiOrigin = resolveWebOrigin(webOrigin);
|
|
86
96
|
const registry = new LocalRunRegistry();
|
|
97
|
+
const directRegistry = new DirectTaskRegistry();
|
|
87
98
|
const authNonce = randomBytes(16).toString("base64url");
|
|
88
99
|
let session = await readSession(apiOrigin, sessionRoot);
|
|
89
100
|
if (session && sessionRequiresWebAuthorization(session, apiOrigin)) session = null;
|
|
@@ -377,6 +388,112 @@ export async function startAgent({
|
|
|
377
388
|
void synchronizeAuthorizedSession();
|
|
378
389
|
return;
|
|
379
390
|
}
|
|
391
|
+
const directExecutionMatch = url.pathname.match(/^\/v1\/direct-execution\/([^/]+)$/);
|
|
392
|
+
if (req.method === "GET" && directExecutionMatch) {
|
|
393
|
+
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
394
|
+
json(res, 401, { error: "simy session expired; run simy again" });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const task = directRegistry.get(decodeURIComponent(directExecutionMatch[1]));
|
|
398
|
+
if (!task) {
|
|
399
|
+
json(res, 404, { error: "direct task not found" });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
json(res, 200, { task: directTaskSnapshot(task) });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (req.method === "POST" && url.pathname === "/v1/direct-execution/start") {
|
|
406
|
+
if (!acceptsNewWork(updateManager)) {
|
|
407
|
+
json(res, 503, updateInProgressResponse(updateManager));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
411
|
+
json(res, 401, { error: "simy session expired; run simy again" });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const body = await readJson(req);
|
|
415
|
+
const instruction = String(body.instruction || "").trim();
|
|
416
|
+
if (!instruction) {
|
|
417
|
+
json(res, 400, { error: "instruction is required" });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (body.backend !== "codex" && body.backend !== "claude") {
|
|
421
|
+
json(res, 400, { error: "backend must be codex or claude" });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const guardrail = classifyExecutionRequirement(instruction);
|
|
425
|
+
if (
|
|
426
|
+
guardrail.decision !== "direct_executor" ||
|
|
427
|
+
body.guardrail?.policy_version !== EXECUTION_GUARDRAIL_POLICY_VERSION ||
|
|
428
|
+
body.guardrail?.decision !== "direct_executor"
|
|
429
|
+
) {
|
|
430
|
+
json(res, 409, {
|
|
431
|
+
error: "This request is not authorized for one-time direct execution.",
|
|
432
|
+
code: "DIRECT_EXECUTION_GUARD_REQUIRED",
|
|
433
|
+
guardrail,
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (
|
|
438
|
+
body.operation !== guardrail.operation ||
|
|
439
|
+
body.permission_mode !== guardrail.permission_mode ||
|
|
440
|
+
body.max_provider_invocations !== 1
|
|
441
|
+
) {
|
|
442
|
+
json(res, 409, {
|
|
443
|
+
error: "Direct execution permissions do not match the pre-guardrail decision.",
|
|
444
|
+
code: "DIRECT_EXECUTION_GUARD_MISMATCH",
|
|
445
|
+
guardrail,
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const inspection = normalizeBackendInspection(body.backend, availableCapabilities);
|
|
450
|
+
if (!inspection.compatible) {
|
|
451
|
+
const summary = backendPreflightSummary(body.backend, inspection);
|
|
452
|
+
json(res, 409, {
|
|
453
|
+
error: summary,
|
|
454
|
+
code: `desktop_executor_${inspection.status}`,
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const repository = String(body.repository || "").trim() || null;
|
|
459
|
+
if (guardrail.repository_required && !repository) {
|
|
460
|
+
json(res, 400, { error: "repository is required for this direct task" });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const indexedRepository = repository ? await ensureRepositoryIndexed(repository) : null;
|
|
464
|
+
if (repository && !indexedRepository?.local_path) {
|
|
465
|
+
json(res, 409, {
|
|
466
|
+
error: `${repository} is not authorized on this SIMY CLI.`,
|
|
467
|
+
code: "repository_not_authorized",
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const repositoryPath = indexedRepository?.local_path || workspace.localPath;
|
|
472
|
+
if (!repositoryPath) {
|
|
473
|
+
json(res, 409, {
|
|
474
|
+
error: "No local workspace is available for this direct task.",
|
|
475
|
+
code: "local_workspace_unavailable",
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const task = createDirectTask({
|
|
480
|
+
instruction,
|
|
481
|
+
backend: body.backend,
|
|
482
|
+
operation: guardrail.operation,
|
|
483
|
+
permissionMode: guardrail.permission_mode,
|
|
484
|
+
repository,
|
|
485
|
+
repositoryPath,
|
|
486
|
+
});
|
|
487
|
+
directRegistry.create(task);
|
|
488
|
+
const runDirectTask = dependencies.runDirectTask || startDirectTask;
|
|
489
|
+
void Promise.resolve(runDirectTask(task)).catch((error) => {
|
|
490
|
+
task.status = "failed";
|
|
491
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
492
|
+
task.updated_at = new Date().toISOString();
|
|
493
|
+
});
|
|
494
|
+
json(res, 202, { ok: true, task: directTaskSnapshot(task) });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
380
497
|
if (req.method === "POST" && agenticLoopPath === "/v1/agentic-loop/start") {
|
|
381
498
|
if (!acceptsNewWork(updateManager)) {
|
|
382
499
|
json(res, 503, updateInProgressResponse(updateManager));
|
|
@@ -387,6 +504,22 @@ export async function startAgent({
|
|
|
387
504
|
return;
|
|
388
505
|
}
|
|
389
506
|
const { body, attachments } = await readCodingLoopStart(req);
|
|
507
|
+
const guardrailSource = String(body.source_message || "").trim();
|
|
508
|
+
if (guardrailSource) {
|
|
509
|
+
const guardrail = classifyExecutionRequirement(guardrailSource);
|
|
510
|
+
if (
|
|
511
|
+
guardrail.decision !== "agentic_loop" ||
|
|
512
|
+
body.guardrail?.policy_version !== EXECUTION_GUARDRAIL_POLICY_VERSION ||
|
|
513
|
+
body.guardrail?.decision !== "agentic_loop"
|
|
514
|
+
) {
|
|
515
|
+
json(res, 409, {
|
|
516
|
+
error: "This task does not qualify for an Agentic Loop.",
|
|
517
|
+
code: "AGENTIC_LOOP_NOT_REQUIRED",
|
|
518
|
+
guardrail,
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
390
523
|
const verified = await verifyLaunchChallenge({
|
|
391
524
|
apiOrigin,
|
|
392
525
|
apiBaseUrl: session.api_base_url,
|
|
@@ -791,6 +924,7 @@ export async function startAgent({
|
|
|
791
924
|
server,
|
|
792
925
|
port,
|
|
793
926
|
registry,
|
|
927
|
+
directRegistry,
|
|
794
928
|
webOrigin: apiOrigin,
|
|
795
929
|
get loginUrl() {
|
|
796
930
|
return loginUrl?.toString() ?? null;
|
|
@@ -1077,6 +1211,7 @@ async function capabilities() {
|
|
|
1077
1211
|
repository_scan_approval: true,
|
|
1078
1212
|
executor_version_preflight: true,
|
|
1079
1213
|
desktop_executor: true,
|
|
1214
|
+
direct_execution: true,
|
|
1080
1215
|
},
|
|
1081
1216
|
session_ttl_hours: 48,
|
|
1082
1217
|
};
|
package/src/cli-contract.js
CHANGED
package/src/desktop-executor.js
CHANGED
|
@@ -49,11 +49,18 @@ export async function resolveDesktopExecutorCommand({
|
|
|
49
49
|
backend,
|
|
50
50
|
instruction,
|
|
51
51
|
repositoryPath,
|
|
52
|
+
executionKind = "agentic_loop",
|
|
53
|
+
permissionMode = "workspace_write",
|
|
52
54
|
environment = process.env,
|
|
53
55
|
spawnImpl = spawn,
|
|
54
56
|
inspectionOptions = {},
|
|
55
57
|
} = {}) {
|
|
56
58
|
const provider = normalizeBackend(backend);
|
|
59
|
+
const directExecution = executionKind === "direct";
|
|
60
|
+
if (executionKind !== "agentic_loop" && !directExecution) {
|
|
61
|
+
throw new Error("Desktop execution kind must be agentic_loop or direct.");
|
|
62
|
+
}
|
|
63
|
+
const normalizedPermissionMode = normalizePermissionMode(permissionMode);
|
|
57
64
|
const target = normalizeDesktopExecutionTarget(DESKTOP_EXECUTION_TARGET);
|
|
58
65
|
const override =
|
|
59
66
|
provider === "claude" ? environment.SIMY_CLAUDE_COMMAND : environment.SIMY_CODEX_COMMAND;
|
|
@@ -62,6 +69,8 @@ export async function resolveDesktopExecutorCommand({
|
|
|
62
69
|
...shellCommand(override, repositoryPath, instruction, spawnImpl),
|
|
63
70
|
backend: provider,
|
|
64
71
|
execution_target: target,
|
|
72
|
+
execution_kind: executionKind,
|
|
73
|
+
permission_mode: normalizedPermissionMode,
|
|
65
74
|
verification: "explicit_command_override",
|
|
66
75
|
};
|
|
67
76
|
}
|
|
@@ -77,12 +86,24 @@ export async function resolveDesktopExecutorCommand({
|
|
|
77
86
|
bin: inspection.executable,
|
|
78
87
|
args:
|
|
79
88
|
provider === "claude"
|
|
80
|
-
?
|
|
81
|
-
|
|
89
|
+
? directExecution
|
|
90
|
+
? claudeDirectBackendArgs(instruction, normalizedPermissionMode)
|
|
91
|
+
: claudeBackendArgs(instruction)
|
|
92
|
+
: directExecution
|
|
93
|
+
? [
|
|
94
|
+
"exec",
|
|
95
|
+
"--json",
|
|
96
|
+
"--sandbox",
|
|
97
|
+
normalizedPermissionMode === "read_only" ? "read-only" : "workspace-write",
|
|
98
|
+
instruction,
|
|
99
|
+
]
|
|
100
|
+
: ["exec", "--json", instruction],
|
|
82
101
|
env: {},
|
|
83
102
|
spawn: spawnImpl,
|
|
84
103
|
backend: provider,
|
|
85
104
|
execution_target: target,
|
|
105
|
+
execution_kind: executionKind,
|
|
106
|
+
permission_mode: normalizedPermissionMode,
|
|
86
107
|
verification: "compatible_version",
|
|
87
108
|
installed_version: inspection.installed_version,
|
|
88
109
|
};
|
|
@@ -102,11 +123,30 @@ export function claudeBackendArgs(instruction) {
|
|
|
102
123
|
];
|
|
103
124
|
}
|
|
104
125
|
|
|
126
|
+
export function claudeDirectBackendArgs(instruction, permissionMode = "read_only") {
|
|
127
|
+
const normalized = normalizePermissionMode(permissionMode);
|
|
128
|
+
return [
|
|
129
|
+
"-p",
|
|
130
|
+
instruction,
|
|
131
|
+
"--output-format",
|
|
132
|
+
"stream-json",
|
|
133
|
+
"--verbose",
|
|
134
|
+
...(normalized === "read_only"
|
|
135
|
+
? ["--permission-mode", "plan"]
|
|
136
|
+
: ["--dangerously-skip-permissions"]),
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
|
|
105
140
|
function normalizeBackend(backend) {
|
|
106
141
|
if (backend === "codex" || backend === "claude") return backend;
|
|
107
142
|
throw new Error("Desktop executor backend must be codex or claude.");
|
|
108
143
|
}
|
|
109
144
|
|
|
145
|
+
function normalizePermissionMode(value) {
|
|
146
|
+
if (value === "read_only" || value === "workspace_write") return value;
|
|
147
|
+
throw new Error("Desktop permission mode must be read_only or workspace_write.");
|
|
148
|
+
}
|
|
149
|
+
|
|
110
150
|
function desktopExecutorUnavailableError(inspection) {
|
|
111
151
|
const provider = inspection.provider_label;
|
|
112
152
|
let message;
|
|
@@ -131,6 +171,7 @@ function shellCommand(command, cwd, instruction, spawnImpl) {
|
|
|
131
171
|
env: {
|
|
132
172
|
SIMY_AGENTIC_LOOP_REQUIREMENT: instruction,
|
|
133
173
|
SIMY_CODING_LOOP_REQUIREMENT: instruction,
|
|
174
|
+
SIMY_DIRECT_EXECUTION_INSTRUCTION: instruction,
|
|
134
175
|
},
|
|
135
176
|
spawn: spawnImpl,
|
|
136
177
|
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { resolveDesktopExecutorCommand } from "./desktop-executor.js";
|
|
4
|
+
import { createProviderStreamDecoder } from "./provider-stream.js";
|
|
5
|
+
|
|
6
|
+
const MAX_OUTPUT_LINES = 400;
|
|
7
|
+
|
|
8
|
+
export class DirectTaskRegistry {
|
|
9
|
+
#tasks = new Map();
|
|
10
|
+
|
|
11
|
+
create(task) {
|
|
12
|
+
if (this.#tasks.has(task.id)) throw new Error(`Direct task ${task.id} already exists.`);
|
|
13
|
+
this.#tasks.set(task.id, task);
|
|
14
|
+
return task;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(taskId) {
|
|
18
|
+
return this.#tasks.get(taskId) ?? null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createDirectTask({
|
|
23
|
+
instruction,
|
|
24
|
+
backend,
|
|
25
|
+
operation,
|
|
26
|
+
permissionMode,
|
|
27
|
+
repository = null,
|
|
28
|
+
repositoryPath,
|
|
29
|
+
}) {
|
|
30
|
+
const now = new Date().toISOString();
|
|
31
|
+
return {
|
|
32
|
+
id: `direct_task_${randomUUID()}`,
|
|
33
|
+
status: "queued",
|
|
34
|
+
instruction,
|
|
35
|
+
backend,
|
|
36
|
+
operation,
|
|
37
|
+
permission_mode: permissionMode,
|
|
38
|
+
repository,
|
|
39
|
+
repository_path: repositoryPath,
|
|
40
|
+
invocation_count: 0,
|
|
41
|
+
output: [],
|
|
42
|
+
result: null,
|
|
43
|
+
error: null,
|
|
44
|
+
token_usage: { records: [] },
|
|
45
|
+
created_at: now,
|
|
46
|
+
updated_at: now,
|
|
47
|
+
child: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function startDirectTask(
|
|
52
|
+
task,
|
|
53
|
+
{ resolveCommand = resolveDesktopExecutorCommand, environment = process.env } = {},
|
|
54
|
+
) {
|
|
55
|
+
if (!task || task.status !== "queued") throw new Error("Direct task must be queued.");
|
|
56
|
+
if (task.invocation_count >= 1) throw new Error("Direct tasks allow only one Provider invocation.");
|
|
57
|
+
const command = await resolveCommand({
|
|
58
|
+
backend: task.backend,
|
|
59
|
+
instruction: task.instruction,
|
|
60
|
+
repositoryPath: task.repository_path,
|
|
61
|
+
executionKind: "direct",
|
|
62
|
+
permissionMode: task.permission_mode,
|
|
63
|
+
environment,
|
|
64
|
+
});
|
|
65
|
+
task.status = "running";
|
|
66
|
+
task.invocation_count = 1;
|
|
67
|
+
task.updated_at = new Date().toISOString();
|
|
68
|
+
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
let settled = false;
|
|
71
|
+
const rawStdout = [];
|
|
72
|
+
const tokenRecords = [];
|
|
73
|
+
const child = command.spawn(command.bin, command.args, {
|
|
74
|
+
cwd: task.repository_path,
|
|
75
|
+
env: { ...process.env, ...command.env },
|
|
76
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
77
|
+
detached: process.platform !== "win32",
|
|
78
|
+
});
|
|
79
|
+
task.child = child;
|
|
80
|
+
const onLine = (line) => {
|
|
81
|
+
const normalized = String(line || "").trim();
|
|
82
|
+
if (!normalized) return;
|
|
83
|
+
task.output.push(normalized);
|
|
84
|
+
if (task.output.length > MAX_OUTPUT_LINES) {
|
|
85
|
+
task.output.splice(0, task.output.length - MAX_OUTPUT_LINES);
|
|
86
|
+
}
|
|
87
|
+
task.updated_at = new Date().toISOString();
|
|
88
|
+
};
|
|
89
|
+
const stdoutDecoder = createProviderStreamDecoder({
|
|
90
|
+
backend: task.backend,
|
|
91
|
+
stream: "stdout",
|
|
92
|
+
onLine,
|
|
93
|
+
onUsage: (usage) => tokenRecords.push({ ...usage, phase: "direct", backend: task.backend }),
|
|
94
|
+
});
|
|
95
|
+
const stderrDecoder = createProviderStreamDecoder({
|
|
96
|
+
backend: task.backend,
|
|
97
|
+
stream: "stderr",
|
|
98
|
+
onLine,
|
|
99
|
+
});
|
|
100
|
+
child.stdout?.on("data", (chunk) => {
|
|
101
|
+
const text = chunk.toString("utf8");
|
|
102
|
+
rawStdout.push(text);
|
|
103
|
+
stdoutDecoder.push(text);
|
|
104
|
+
});
|
|
105
|
+
child.stderr?.on("data", (chunk) => stderrDecoder.push(chunk.toString("utf8")));
|
|
106
|
+
|
|
107
|
+
const finish = ({ exitCode = null, error = null } = {}) => {
|
|
108
|
+
if (settled) return;
|
|
109
|
+
settled = true;
|
|
110
|
+
stdoutDecoder.flush();
|
|
111
|
+
stderrDecoder.flush();
|
|
112
|
+
task.child = null;
|
|
113
|
+
task.token_usage = { records: tokenRecords };
|
|
114
|
+
task.status = error || exitCode !== 0 ? "failed" : "succeeded";
|
|
115
|
+
task.error = error || (exitCode !== 0 ? `Provider exited with code ${exitCode}.` : null);
|
|
116
|
+
task.result = task.output.at(-1) || rawStdout.join("").trim() || null;
|
|
117
|
+
task.updated_at = new Date().toISOString();
|
|
118
|
+
resolve(task);
|
|
119
|
+
};
|
|
120
|
+
child.on("error", (error) => finish({ error: error.message }));
|
|
121
|
+
child.on("close", (code) => finish({ exitCode: code }));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function directTaskSnapshot(task) {
|
|
126
|
+
return {
|
|
127
|
+
id: task.id,
|
|
128
|
+
status: task.status,
|
|
129
|
+
instruction: task.instruction,
|
|
130
|
+
backend: task.backend,
|
|
131
|
+
operation: task.operation,
|
|
132
|
+
permission_mode: task.permission_mode,
|
|
133
|
+
repository: task.repository,
|
|
134
|
+
invocation_count: task.invocation_count,
|
|
135
|
+
output: [...task.output],
|
|
136
|
+
result: task.result,
|
|
137
|
+
error: task.error,
|
|
138
|
+
token_usage: structuredClone(task.token_usage),
|
|
139
|
+
created_at: task.created_at,
|
|
140
|
+
updated_at: task.updated_at,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const EXECUTION_GUARDRAIL_POLICY_VERSION = "2026-07-21.1";
|
|
2
|
+
|
|
3
|
+
const OBSERVE_PATTERN =
|
|
4
|
+
/\b(monitor|watch|observe|track|status|progress|keep an eye on)\b|監視|見守|進捗|状況を?(?:見|確認)|监控|監控|观察|觀察|进度|進度|状态|狀態/i;
|
|
5
|
+
const READ_PATTERN =
|
|
6
|
+
/\b(explain|summari[sz]e|inspect|review|look at|read|check|diagnose|investigate)\b|説明|要約|確認|調査|レビュー|查看|解释|說明|总结|總結|检查|檢查|调查|調查|诊断|診斷/i;
|
|
7
|
+
const CHANGE_PATTERN =
|
|
8
|
+
/\b(implement|fix|build|create|change|update|refactor|add|remove|rename|ship|write|edit|merge|release|publish)\b|実装|修正|改善|変更|追加|削除|作成|開発|マージ|公開|发布|發佈|实现|實現|修复|修復|修改|改掉|改一下|新增|删除|刪除|重构|重構|合并|合併/i;
|
|
9
|
+
const OBJECT_PATTERN =
|
|
10
|
+
/\b(code|file|readme|api|ui|ux|feature|bug|pr|pull request|repository|repo|branch|test|migration|endpoint|component|page|cli|backend|extension)\b|コード|ファイル|機能|不具合|画面|ページ|テスト|移行|组件|組件|页面|頁面|代码|代碼|文件|功能|错误|錯誤|仓库|倉庫|分支/i;
|
|
11
|
+
const VAGUE_PATTERN = /^(?:fix|do|handle|continue|change)?\s*(?:it|this|that)?[.!。!]?$/i;
|
|
12
|
+
const VAGUE_CJK_PATTERN = /^(?:これ|それ|あれ|やって|直して|続けて|这个|這個|那个|那個|处理|處理|修一下)[。!]?$/i;
|
|
13
|
+
const UNSUPPORTED_PATTERN =
|
|
14
|
+
/\b(?:steal|exfiltrate|bypass authentication|disable audit)\b|認証を迂回|監査を無効|窃取凭证|繞過認證/i;
|
|
15
|
+
|
|
16
|
+
const MANAGED_SIGNAL_PATTERNS = [
|
|
17
|
+
["pr_merge_release", /\b(?:pull request|pr|merge|release|publish|deploy)\b|PR|マージ|リリース|デプロイ|合并|合併|发布|發佈|部署/i],
|
|
18
|
+
["verification", /\b(?:test|e2e|integration test|until green|ci)\b|テスト|検証|通过测试|通過測試|直到.*通过|直到.*通過/i],
|
|
19
|
+
["browser_evidence", /\b(?:browser|screenshots?|visual evidence|evidence)\b|ブラウザ|スクリーンショット|証跡|截图|截圖|浏览器|瀏覽器|证据|證據/i],
|
|
20
|
+
["cross_repository", /\b(?:cross[- ]repo|multiple repos|multi[- ]repo)\b|複数(?:の)?リポジトリ|跨仓库|跨倉庫|多个仓库|多個倉庫/i],
|
|
21
|
+
["high_risk_change", /\b(?:migration|authentication|authorization|security|rollback)\b|移行|認証|認可|セキュリティ|ロールバック|迁移|遷移|认证|認證|授权|授權|安全|回滚|回滾/i],
|
|
22
|
+
["diagnose_repair_reverify", /\bdiagnos\w*\b[\s\S]*\bfix\w*\b[\s\S]*\b(?:verify|retest)\b|調査[\s\S]*修正[\s\S]*再検証|诊断[\s\S]*修复[\s\S]*复验|診斷[\s\S]*修復[\s\S]*複驗/i],
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function classifyExecutionRequirement(sourceMessage) {
|
|
26
|
+
const source = String(sourceMessage || "")
|
|
27
|
+
.replace(/^\s*\/(?:agentic-loop|coding-loop)\b\s*/i, "")
|
|
28
|
+
.trim();
|
|
29
|
+
if (!source || VAGUE_PATTERN.test(source) || VAGUE_CJK_PATTERN.test(source)) {
|
|
30
|
+
const vagueChangeRequested = CHANGE_PATTERN.test(source);
|
|
31
|
+
return guardrail("needs_clarification", {
|
|
32
|
+
change_requested: vagueChangeRequested,
|
|
33
|
+
operation: vagueChangeRequested ? "execute" : "read",
|
|
34
|
+
permission_mode: vagueChangeRequested ? "workspace_write" : "read_only",
|
|
35
|
+
reason_code: "goal_not_clear",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (UNSUPPORTED_PATTERN.test(source)) {
|
|
39
|
+
return guardrail("unsupported", {
|
|
40
|
+
operation: "execute",
|
|
41
|
+
permission_mode: "workspace_write",
|
|
42
|
+
reason_code: "prohibited_operation",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const changeRequested = CHANGE_PATTERN.test(source);
|
|
46
|
+
const observeRequested = OBSERVE_PATTERN.test(source);
|
|
47
|
+
const readRequested = READ_PATTERN.test(source);
|
|
48
|
+
const hasObject = OBJECT_PATTERN.test(source) || source.length >= 24;
|
|
49
|
+
const agenticSignals = MANAGED_SIGNAL_PATTERNS.filter(([, pattern]) => pattern.test(source)).map(
|
|
50
|
+
([signal]) => signal,
|
|
51
|
+
);
|
|
52
|
+
if (changeRequested && hasObject && agenticSignals.length >= 2) {
|
|
53
|
+
return guardrail("agentic_loop", {
|
|
54
|
+
change_requested: true,
|
|
55
|
+
managed_lifecycle_required: true,
|
|
56
|
+
repository_required: true,
|
|
57
|
+
operation: "execute",
|
|
58
|
+
permission_mode: "workspace_write",
|
|
59
|
+
agentic_signals: agenticSignals,
|
|
60
|
+
reason_code: "managed_change_lifecycle_required",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (observeRequested && !changeRequested) {
|
|
64
|
+
return guardrail("direct_executor", {
|
|
65
|
+
repository_required: /\b(?:repo|repository|pr|branch)\b|リポジトリ|仓库|倉庫|分支/i.test(source),
|
|
66
|
+
operation: "observe",
|
|
67
|
+
permission_mode: "read_only",
|
|
68
|
+
direct_signals: ["bounded_observation", "no_persistent_change"],
|
|
69
|
+
reason_code: "observe_existing_provider_session",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (changeRequested && hasObject) {
|
|
73
|
+
return guardrail("direct_executor", {
|
|
74
|
+
change_requested: true,
|
|
75
|
+
repository_required: true,
|
|
76
|
+
operation: "execute",
|
|
77
|
+
permission_mode: "workspace_write",
|
|
78
|
+
agentic_signals: agenticSignals,
|
|
79
|
+
direct_signals: ["single_bounded_change", "managed_lifecycle_not_required"],
|
|
80
|
+
reason_code: "bounded_change_direct_execution",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (changeRequested) {
|
|
84
|
+
return guardrail("needs_clarification", {
|
|
85
|
+
change_requested: true,
|
|
86
|
+
operation: "execute",
|
|
87
|
+
permission_mode: "workspace_write",
|
|
88
|
+
reason_code: "change_target_missing",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (readRequested || source.length >= 8) {
|
|
92
|
+
return guardrail("direct_executor", {
|
|
93
|
+
repository_required: /\b(?:repo|repository|pr|file|code)\b|コード|ファイル|仓库|倉庫|文件|代码|代碼/i.test(source),
|
|
94
|
+
operation: "read",
|
|
95
|
+
permission_mode: "read_only",
|
|
96
|
+
direct_signals: ["single_read_task", "no_managed_lifecycle"],
|
|
97
|
+
reason_code: "bounded_read_direct_execution",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return guardrail("needs_clarification", {
|
|
101
|
+
operation: "read",
|
|
102
|
+
permission_mode: "read_only",
|
|
103
|
+
reason_code: "action_missing",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function guardrail(decision, overrides) {
|
|
108
|
+
return {
|
|
109
|
+
policy_version: EXECUTION_GUARDRAIL_POLICY_VERSION,
|
|
110
|
+
decision,
|
|
111
|
+
change_requested: false,
|
|
112
|
+
managed_lifecycle_required: false,
|
|
113
|
+
repository_required: false,
|
|
114
|
+
operation: "read",
|
|
115
|
+
permission_mode: "read_only",
|
|
116
|
+
agentic_signals: [],
|
|
117
|
+
direct_signals: [],
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
}
|