create-walle 0.9.25 → 0.9.26
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/bin/create-walle.js +815 -45
- package/package.json +2 -2
- package/template/bin/ctm-dev-cleanup.js +90 -4
- package/template/bin/ctm-launch.sh +49 -1
- package/template/bin/dev.sh +45 -1
- package/template/bin/ensure-stable-node.js +132 -0
- package/template/bin/install-service.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +899 -119
- package/template/claude-task-manager/approval-agent.js +360 -40
- package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
- package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
- package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
- package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
- package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
- package/template/claude-task-manager/db.js +399 -48
- package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
- package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
- package/template/claude-task-manager/lib/approval-hook.js +200 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
- package/template/claude-task-manager/lib/auth-rules.js +11 -0
- package/template/claude-task-manager/lib/background-llm.js +32 -4
- package/template/claude-task-manager/lib/codesign-identity.js +140 -0
- package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
- package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
- package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
- package/template/claude-task-manager/lib/codex-paths.js +73 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
- package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
- package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
- package/template/claude-task-manager/lib/command-targets.js +163 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
- package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
- package/template/claude-task-manager/lib/escalation-review.js +80 -3
- package/template/claude-task-manager/lib/flow-control.js +52 -0
- package/template/claude-task-manager/lib/fs-watcher.js +24 -15
- package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
- package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
- package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
- package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
- package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
- package/template/claude-task-manager/lib/perf-tracker.js +29 -2
- package/template/claude-task-manager/lib/permission-match.js +146 -16
- package/template/claude-task-manager/lib/project-slug.js +33 -0
- package/template/claude-task-manager/lib/prompt-intent.js +51 -4
- package/template/claude-task-manager/lib/read-pool-client.js +48 -3
- package/template/claude-task-manager/lib/real-node.js +73 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
- package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
- package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
- package/template/claude-task-manager/lib/session-history.js +5 -7
- package/template/claude-task-manager/lib/session-host-manager.js +19 -0
- package/template/claude-task-manager/lib/session-jobs.js +6 -0
- package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
- package/template/claude-task-manager/lib/session-messages-page.js +211 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
- package/template/claude-task-manager/lib/session-standup.js +8 -0
- package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
- package/template/claude-task-manager/lib/session-token-usage.js +30 -8
- package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
- package/template/claude-task-manager/lib/storage-migration.js +2 -1
- package/template/claude-task-manager/lib/transcript-store.js +179 -12
- package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
- package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
- package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
- package/template/claude-task-manager/package.json +5 -2
- package/template/claude-task-manager/prompt-harvest.js +31 -11
- package/template/claude-task-manager/providers/claude-code.js +29 -1
- package/template/claude-task-manager/providers/codex.js +13 -1
- package/template/claude-task-manager/public/css/setup.css +11 -0
- package/template/claude-task-manager/public/css/walle-session.css +132 -4
- package/template/claude-task-manager/public/css/walle.css +89 -0
- package/template/claude-task-manager/public/icon-16.png +0 -0
- package/template/claude-task-manager/public/icon-32.png +0 -0
- package/template/claude-task-manager/public/icon-512.png +0 -0
- package/template/claude-task-manager/public/index.html +2483 -165
- package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
- package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
- package/template/claude-task-manager/public/js/message-renderer.js +60 -1
- package/template/claude-task-manager/public/js/prompts.js +13 -1
- package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
- package/template/claude-task-manager/public/js/setup.js +54 -10
- package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
- package/template/claude-task-manager/public/js/stream-view.js +78 -0
- package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
- package/template/claude-task-manager/public/js/tool-state.js +155 -0
- package/template/claude-task-manager/public/js/walle-session.js +887 -326
- package/template/claude-task-manager/public/js/walle.js +306 -195
- package/template/claude-task-manager/public/m/app.css +1 -0
- package/template/claude-task-manager/public/m/app.js +33 -3
- package/template/claude-task-manager/queue-engine.js +45 -1
- package/template/claude-task-manager/server.js +3367 -540
- package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
- package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
- package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
- package/template/claude-task-manager/workers/session-host-process.js +10 -0
- package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
- package/template/package.json +2 -3
- package/template/shared/icons/AppIcon-ctm.icns +0 -0
- package/template/shared/icons/AppIcon-walle.icns +0 -0
- package/template/wall-e/agent.js +139 -18
- package/template/wall-e/api-walle.js +201 -22
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
- package/template/wall-e/brain.js +1049 -39
- package/template/wall-e/chat.js +427 -86
- package/template/wall-e/coding/acceptance-contract.js +26 -1
- package/template/wall-e/coding/action-memory-policy.js +353 -0
- package/template/wall-e/coding/action-memory-store.js +814 -0
- package/template/wall-e/coding/initial-messages.js +197 -0
- package/template/wall-e/coding/no-progress-guard.js +327 -0
- package/template/wall-e/coding/permission-service.js +88 -22
- package/template/wall-e/coding/session-workspaces.js +81 -0
- package/template/wall-e/coding/shell-sandbox.js +124 -0
- package/template/wall-e/coding/stream-processor.js +63 -2
- package/template/wall-e/coding/tool-execution-controller.js +14 -1
- package/template/wall-e/coding/tool-registry.js +1 -1
- package/template/wall-e/coding/transcript-writer.js +3 -0
- package/template/wall-e/coding-orchestrator.js +636 -35
- package/template/wall-e/coding-prompts.js +51 -2
- package/template/wall-e/docs/model-routing-policy.md +59 -0
- package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
- package/template/wall-e/extraction/knowledge-extractor.js +76 -23
- package/template/wall-e/http/chat-api.js +30 -12
- package/template/wall-e/http/model-admin.js +93 -1
- package/template/wall-e/lib/background-lanes.js +133 -0
- package/template/wall-e/lib/boot-profile.js +11 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
- package/template/wall-e/lib/brain-read-pool-client.js +311 -0
- package/template/wall-e/lib/diagnostics-flags.js +87 -0
- package/template/wall-e/lib/event-loop-monitor.js +74 -3
- package/template/wall-e/lib/mcp-integration.js +7 -1
- package/template/wall-e/lib/real-node.js +98 -0
- package/template/wall-e/lib/runtime-health.js +206 -0
- package/template/wall-e/lib/runtime-worker-pool.js +101 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
- package/template/wall-e/lib/scheduler.js +446 -17
- package/template/wall-e/lib/service-health.js +61 -2
- package/template/wall-e/lib/service-readiness.js +258 -0
- package/template/wall-e/lib/usage.js +152 -0
- package/template/wall-e/lib/worker-thread-pool.js +389 -0
- package/template/wall-e/llm/client.js +81 -4
- package/template/wall-e/llm/default-fallback.js +54 -8
- package/template/wall-e/llm/mlx.js +536 -73
- package/template/wall-e/llm/mlx.plugin.json +1 -1
- package/template/wall-e/llm/ollama.js +342 -43
- package/template/wall-e/llm/provider-error.js +18 -1
- package/template/wall-e/llm/provider-health-state.js +176 -0
- package/template/wall-e/llm/routing-policy.js +796 -0
- package/template/wall-e/llm/supported-models.js +5 -0
- package/template/wall-e/loops/tasks.js +60 -14
- package/template/wall-e/loops/think.js +89 -24
- package/template/wall-e/mcp-server.js +192 -28
- package/template/wall-e/server.js +32 -7
- package/template/wall-e/skills/script-skill-runner.js +8 -1
- package/template/wall-e/skills/skill-planner.js +64 -1
- package/template/wall-e/tools/builtin-middleware.js +67 -2
- package/template/wall-e/tools/local-tools.js +116 -26
- package/template/wall-e/tools/permission-checker.js +52 -4
- package/template/wall-e/tools/permission-rules.js +36 -0
- package/template/wall-e/tools/shell-analyzer.js +46 -1
- package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
- package/template/wall-e/training/real-trajectory-miner.js +2617 -0
- package/template/wall-e/training/replay-eval-analysis.js +151 -0
- package/template/wall-e/training/run-shell-command-selector.js +277 -0
- package/template/wall-e/training/tool-sft-dataset.js +312 -0
- package/template/wall-e/training/tool-sft-renderers.js +144 -0
- package/template/wall-e/training/tool-trace-harvester.js +1440 -0
- package/template/wall-e/training/trajectory-action-selector.js +364 -0
- package/template/wall-e/weather-runtime.js +232 -0
- package/template/wall-e/workers/brain-owner-worker.js +162 -0
- package/template/wall-e/workers/brain-read-worker.js +148 -0
- package/template/wall-e/workers/runtime-worker.js +145 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// macOS code-signing identity helpers, shared between the CTM server (api-prompts.js, for the
|
|
4
|
+
// hotkey + screen-auth helpers) and the standalone daemon-node provisioner (bin/ensure-stable-node.js).
|
|
5
|
+
// Kept dependency-free (only Node built-ins) so the provisioner can require it WITHOUT pulling in
|
|
6
|
+
// the whole server module (db, queue-engine, approval-agent, …).
|
|
7
|
+
//
|
|
8
|
+
// Why a stable identity matters: a Developer-ID signature carries a Team Identifier, giving a
|
|
9
|
+
// binary a stable, OS-trusted Designated Requirement. macOS keys a TCC grant to that requirement,
|
|
10
|
+
// so a Developer-ID (or notarized) binary keeps its grant across rebuilds/restarts; a self-signed
|
|
11
|
+
// or ad-hoc binary does not. Without a Developer ID (most end users) we fall back to a stable
|
|
12
|
+
// self-signed local cert.
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const SIGN_IDENTITY_CN = 'Wall-E Local Signing';
|
|
20
|
+
const SIGN_KEYCHAIN = path.join(os.homedir(), '.walle', 'walle-signing.keychain-db');
|
|
21
|
+
|
|
22
|
+
function signLocalCtmBinary(binaryPath, identifier) {
|
|
23
|
+
if (process.platform !== 'darwin' || !fs.existsSync(SIGN_KEYCHAIN)) return false;
|
|
24
|
+
try {
|
|
25
|
+
try {
|
|
26
|
+
execFileSync('security', ['unlock-keychain', '-p', 'walle', SIGN_KEYCHAIN], { stdio: 'ignore' });
|
|
27
|
+
} catch {}
|
|
28
|
+
const certs = execFileSync(
|
|
29
|
+
'security',
|
|
30
|
+
['find-certificate', '-a', '-c', SIGN_IDENTITY_CN, '-Z', SIGN_KEYCHAIN],
|
|
31
|
+
{ encoding: 'utf8' },
|
|
32
|
+
);
|
|
33
|
+
const match = certs.match(/SHA-1 hash:\s*([0-9A-Fa-f]{40})/);
|
|
34
|
+
if (!match) return false;
|
|
35
|
+
try {
|
|
36
|
+
execFileSync(
|
|
37
|
+
'security',
|
|
38
|
+
['set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', '-k', 'walle', SIGN_KEYCHAIN],
|
|
39
|
+
{ stdio: 'ignore' },
|
|
40
|
+
);
|
|
41
|
+
} catch {}
|
|
42
|
+
execFileSync(
|
|
43
|
+
'codesign',
|
|
44
|
+
['--force', '--sign', match[1], '--identifier', identifier, '--keychain', SIGN_KEYCHAIN, binaryPath],
|
|
45
|
+
{ stdio: 'ignore', timeout: 120000 },
|
|
46
|
+
);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Returns the SHA-1 hash of an available "Developer ID Application" codesigning identity, or ''.
|
|
54
|
+
function developerIdIdentityHash() {
|
|
55
|
+
if (process.platform !== 'darwin') return '';
|
|
56
|
+
try {
|
|
57
|
+
const out = execFileSync('security', ['find-identity', '-v', '-p', 'codesigning'], { encoding: 'utf8' });
|
|
58
|
+
const match = out.match(/([0-9A-Fa-f]{40})\s+"Developer ID Application:[^"]*"/);
|
|
59
|
+
return match ? match[1] : '';
|
|
60
|
+
} catch {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Sign a local CTM helper with the best available identity: Developer ID first (stable team id
|
|
66
|
+
// → persistent TCC grant + honored system prompt), else the stable self-signed local cert.
|
|
67
|
+
function signCtmBinaryPreferDeveloperId(binaryPath, identifier) {
|
|
68
|
+
const devId = developerIdIdentityHash();
|
|
69
|
+
if (devId) {
|
|
70
|
+
try {
|
|
71
|
+
execFileSync(
|
|
72
|
+
'codesign',
|
|
73
|
+
['--force', '--sign', devId, '--identifier', identifier, binaryPath],
|
|
74
|
+
{ stdio: 'ignore', timeout: 120000 },
|
|
75
|
+
);
|
|
76
|
+
return { signed: true, identity: 'developer-id' };
|
|
77
|
+
} catch {}
|
|
78
|
+
}
|
|
79
|
+
const ok = signLocalCtmBinary(binaryPath, identifier);
|
|
80
|
+
return { signed: ok, identity: ok ? 'self-signed' : null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Developer-ID-sign a whole .app bundle DIRECTORY (with --deep, so the cloned node exec AND its
|
|
84
|
+
// co-located @rpath dylibs in Contents/lib are all covered). Used to give a branded daemon bundle
|
|
85
|
+
// (Coding Task Manager.app / Wall-E.app) a STABLE Team Identifier so its TCC grants persist AND
|
|
86
|
+
// macOS keeps showing the bundle's CFBundleName in prompts (Info.plist is untouched by signing).
|
|
87
|
+
// Returns { signed, identity }. No-op without a Developer ID (returns { signed:false }) — callers
|
|
88
|
+
// fall back to the notarized bare node; we never self-sign here (self-signed gives no stable Team
|
|
89
|
+
// ID and re-introduces the per-restart re-prompt this is meant to fix).
|
|
90
|
+
function devIdSignBundle(bundleAppDir, identifier) {
|
|
91
|
+
const devId = developerIdIdentityHash();
|
|
92
|
+
if (!devId) return { signed: false, identity: null };
|
|
93
|
+
try {
|
|
94
|
+
execFileSync(
|
|
95
|
+
'codesign',
|
|
96
|
+
['--force', '--deep', '--sign', devId, '--identifier', identifier, bundleAppDir],
|
|
97
|
+
{ stdio: 'ignore', timeout: 120000 },
|
|
98
|
+
);
|
|
99
|
+
return { signed: true, identity: 'developer-id' };
|
|
100
|
+
} catch {
|
|
101
|
+
return { signed: false, identity: null };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function localCodeSignIdentifier(binaryPath) {
|
|
106
|
+
if (process.platform !== 'darwin' || !binaryPath || !fs.existsSync(binaryPath)) return '';
|
|
107
|
+
try {
|
|
108
|
+
const out = spawnSync('codesign', ['-dv', '--verbose=4', binaryPath], { encoding: 'utf8' });
|
|
109
|
+
const text = `${out.stdout || ''}\n${out.stderr || ''}`;
|
|
110
|
+
const match = text.match(/^Identifier=(.+)$/m);
|
|
111
|
+
return match ? match[1].trim() : '';
|
|
112
|
+
} catch {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The signing Team Identifier, or '' when unsigned/self-signed. Present ⇒ Developer-ID signed.
|
|
118
|
+
function localCodeSignTeamId(binaryPath) {
|
|
119
|
+
if (process.platform !== 'darwin' || !binaryPath || !fs.existsSync(binaryPath)) return '';
|
|
120
|
+
try {
|
|
121
|
+
const out = spawnSync('codesign', ['-dv', '--verbose=4', binaryPath], { encoding: 'utf8' });
|
|
122
|
+
const text = `${out.stdout || ''}\n${out.stderr || ''}`;
|
|
123
|
+
const match = text.match(/^TeamIdentifier=(.+)$/m);
|
|
124
|
+
const team = match ? match[1].trim() : '';
|
|
125
|
+
return team === 'not set' ? '' : team;
|
|
126
|
+
} catch {
|
|
127
|
+
return '';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
SIGN_IDENTITY_CN,
|
|
133
|
+
SIGN_KEYCHAIN,
|
|
134
|
+
signLocalCtmBinary,
|
|
135
|
+
developerIdIdentityHash,
|
|
136
|
+
signCtmBinaryPreferDeveloperId,
|
|
137
|
+
devIdSignBundle,
|
|
138
|
+
localCodeSignIdentifier,
|
|
139
|
+
localCodeSignTeamId,
|
|
140
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Codex app-server JSON-RPC client (driven mode).
|
|
4
|
+
//
|
|
5
|
+
// Spawns `codex app-server` and drives a conversation over newline-delimited JSON-RPC
|
|
6
|
+
// stdio, answering the server-initiated approval requests through the shared
|
|
7
|
+
// decideApproval cascade (via codex-approval-bridge) instead of PTY-scraping the TUI.
|
|
8
|
+
// This is the engine for a future CTM-driven-Codex session type; the message framing,
|
|
9
|
+
// id correlation, and approval dispatch are testable without a real process via the
|
|
10
|
+
// `transport` injection.
|
|
11
|
+
//
|
|
12
|
+
// Validated against codex-cli 0.132.0 (see docs/codex-app-server-approvals.md).
|
|
13
|
+
|
|
14
|
+
const { spawn } = require('child_process');
|
|
15
|
+
const bridge = require('./codex-approval-bridge');
|
|
16
|
+
|
|
17
|
+
// A transport is { write(jsonString), onLine(cb), close() }. The default spawns codex;
|
|
18
|
+
// tests inject a fake one to drive the client deterministically.
|
|
19
|
+
function spawnCodexTransport({ cwd, env, spawnFn = spawn, command = 'codex', args = ['app-server'] } = {}) {
|
|
20
|
+
const child = spawnFn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], cwd, env });
|
|
21
|
+
let buf = '';
|
|
22
|
+
let lineCb = null;
|
|
23
|
+
child.stdout.on('data', (d) => {
|
|
24
|
+
buf += d.toString();
|
|
25
|
+
let i;
|
|
26
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
27
|
+
const line = buf.slice(0, i); buf = buf.slice(i + 1);
|
|
28
|
+
if (line.trim() && lineCb) lineCb(line);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
write: (s) => { try { child.stdin.write(s + '\n'); } catch {} },
|
|
33
|
+
onLine: (cb) => { lineCb = cb; },
|
|
34
|
+
close: () => { try { child.kill(); } catch {} },
|
|
35
|
+
_child: child,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// decide(context, kind) → Promise<'allow'|'deny'|'abort'>. onItem/onNotification get the
|
|
40
|
+
// streamed turn/item events (the UI surface). onApproval (optional) is notified of each
|
|
41
|
+
// approval request + resolution for logging/telemetry.
|
|
42
|
+
function createCodexAppServerClient({ transport, decide, onItem, onNotification, onApproval, log = () => {} } = {}) {
|
|
43
|
+
if (!transport) throw new Error('createCodexAppServerClient: transport required');
|
|
44
|
+
let nextId = 1;
|
|
45
|
+
const pending = new Map(); // id → { resolve, reject }
|
|
46
|
+
|
|
47
|
+
const send = (obj) => transport.write(JSON.stringify(obj));
|
|
48
|
+
function request(method, params) {
|
|
49
|
+
const id = nextId++;
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
pending.set(id, { resolve, reject });
|
|
52
|
+
send({ id, method, params: params || {} });
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function respond(id, result) { send({ id, result }); }
|
|
56
|
+
|
|
57
|
+
async function handleMessage(msg) {
|
|
58
|
+
if (!msg || typeof msg !== 'object') return;
|
|
59
|
+
// 1) a response to one of OUR requests
|
|
60
|
+
if (msg.id != null && (Object.prototype.hasOwnProperty.call(msg, 'result') || msg.error) && pending.has(msg.id)) {
|
|
61
|
+
const p = pending.get(msg.id); pending.delete(msg.id);
|
|
62
|
+
if (msg.error) p.reject(Object.assign(new Error(msg.error.message || 'rpc error'), { rpc: msg.error }));
|
|
63
|
+
else p.resolve(msg.result);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// 2) a SERVER-initiated request (id + method) — approvals live here
|
|
67
|
+
if (msg.id != null && msg.method) {
|
|
68
|
+
const appr = bridge.parseCodexApprovalRequest(msg.method, msg.params || {});
|
|
69
|
+
if (appr) {
|
|
70
|
+
let decision = 'deny';
|
|
71
|
+
try { decision = (await decide(appr.context, appr.kind)) || 'deny'; }
|
|
72
|
+
catch (e) { decision = 'deny'; log('[codex-client] decide() threw, denying:', e.message); }
|
|
73
|
+
respond(msg.id, bridge.buildCodexApprovalResponse(decision, appr.protocol));
|
|
74
|
+
if (onApproval) { try { onApproval({ method: msg.method, kind: appr.kind, decision, context: appr.context }); } catch {} }
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Unknown server request — answer with an empty result so the agent isn't wedged.
|
|
78
|
+
respond(msg.id, {});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// 3) a notification (method, no id)
|
|
82
|
+
if (msg.method) {
|
|
83
|
+
if (/^item\//.test(msg.method)) { if (onItem) { try { onItem(msg.method, msg.params); } catch {} } }
|
|
84
|
+
else if (onNotification) { try { onNotification(msg.method, msg.params); } catch {} }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
transport.onLine((line) => {
|
|
89
|
+
let msg; try { msg = JSON.parse(line); } catch { return; }
|
|
90
|
+
handleMessage(msg);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
async function initialize(clientInfo = { name: 'ctm', version: '1.0.0' }) {
|
|
94
|
+
const result = await request('initialize', { clientInfo });
|
|
95
|
+
send({ method: 'initialized', params: {} }); // notification, no id
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
// Route approvals to us (approvalsReviewer:user) and ask on request; workspace-write
|
|
99
|
+
// sandbox so the agent runs contained by default. Caller may override any field.
|
|
100
|
+
function startThread(opts = {}) {
|
|
101
|
+
return request('thread/start', {
|
|
102
|
+
approvalPolicy: 'on-request', approvalsReviewer: 'user', sandbox: 'workspace-write',
|
|
103
|
+
...opts,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function startTurn(threadId, input, opts = {}) {
|
|
107
|
+
const inp = typeof input === 'string' ? [{ type: 'text', text: input }] : input;
|
|
108
|
+
return request('turn/start', { threadId, input: inp, ...opts });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
initialize, startThread, startTurn, request,
|
|
113
|
+
close: () => transport.close(),
|
|
114
|
+
// exposed for unit tests
|
|
115
|
+
_handleMessage: handleMessage,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { createCodexAppServerClient, spawnCodexTransport };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Codex app-server approval bridge.
|
|
4
|
+
//
|
|
5
|
+
// Codex (OpenAI CLI) exposes a JSON-RPC app-server over stdio. When the agent wants to
|
|
6
|
+
// run a command or apply a patch it sends the CLIENT a server-initiated REQUEST and
|
|
7
|
+
// BLOCKS the turn until the client answers with a decision keyed by the request id.
|
|
8
|
+
// This is Codex's analog of Claude's PreToolUse hook — structured, bound to the exact
|
|
9
|
+
// tool call, no PTY scrape, no TOCTOU.
|
|
10
|
+
//
|
|
11
|
+
// Verified against codex-cli 0.132.0 (see memory ctm-approver-protocol-hook-prototype-
|
|
12
|
+
// validated). The binary is mid-migration: it emits TWO protocol generations depending
|
|
13
|
+
// on what the client negotiates, so a robust bridge handles BOTH:
|
|
14
|
+
// v2 (current): item/commandExecution/requestApproval, item/fileChange/requestApproval,
|
|
15
|
+
// item/permissions/requestApproval — decisions camelCase
|
|
16
|
+
// (accept | acceptForSession | decline | cancel)
|
|
17
|
+
// legacy: execCommandApproval, applyPatchApproval — decisions snake_case
|
|
18
|
+
// (approved | approved_for_session | denied | abort)
|
|
19
|
+
//
|
|
20
|
+
// This module is PURE (no child process / no I/O) so it unit-tests the exact wire
|
|
21
|
+
// mapping. The stdio client that drives a real `codex app-server` composes these.
|
|
22
|
+
|
|
23
|
+
// Recognize an incoming server→client method as an approval request and normalize it
|
|
24
|
+
// to { kind, protocol, context } for the shared decideApproval cascade. Returns null
|
|
25
|
+
// for non-approval methods (notifications, other requests).
|
|
26
|
+
// kind: 'command' | 'patch' | 'permissions'
|
|
27
|
+
// protocol: 'v2' | 'legacy' (decides the response vocabulary)
|
|
28
|
+
// context: { toolName, command, warning, fullContext } (decideApproval input)
|
|
29
|
+
function parseCodexApprovalRequest(method, params = {}) {
|
|
30
|
+
switch (method) {
|
|
31
|
+
case 'item/commandExecution/requestApproval': {
|
|
32
|
+
// v2 gives a `command` display string + parsed commandActions (not raw argv).
|
|
33
|
+
const command = String(params.command || _joinActions(params.commandActions) || '').trim();
|
|
34
|
+
return {
|
|
35
|
+
kind: 'command', protocol: 'v2',
|
|
36
|
+
context: _ctx('Bash command', command, params.reason),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
case 'item/fileChange/requestApproval': {
|
|
40
|
+
return {
|
|
41
|
+
kind: 'patch', protocol: 'v2',
|
|
42
|
+
context: _ctx('Edit', _grantRootLabel(params.grantRoot) || 'apply file changes', params.reason),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
case 'item/permissions/requestApproval': {
|
|
46
|
+
return {
|
|
47
|
+
kind: 'permissions', protocol: 'v2',
|
|
48
|
+
context: _ctx('Permissions', String(params.reason || 'permission escalation'), params.reason),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
case 'execCommandApproval': {
|
|
52
|
+
// legacy carries the real argv array.
|
|
53
|
+
const argv = Array.isArray(params.command) ? params.command : [];
|
|
54
|
+
const command = argv.length ? argv.join(' ') : String(params.command || '').trim();
|
|
55
|
+
return {
|
|
56
|
+
kind: 'command', protocol: 'legacy',
|
|
57
|
+
context: _ctx('Bash command', command, params.reason),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
case 'applyPatchApproval': {
|
|
61
|
+
const files = params.fileChanges && typeof params.fileChanges === 'object'
|
|
62
|
+
? Object.keys(params.fileChanges) : [];
|
|
63
|
+
const command = files.length ? `apply patch to ${files.join(', ')}` : 'apply patch';
|
|
64
|
+
return {
|
|
65
|
+
kind: 'patch', protocol: 'legacy',
|
|
66
|
+
context: _ctx('Edit', command.slice(0, 2000), params.reason),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
default:
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Map our shared decideApproval() result to the JSON-RPC result VALUE Codex expects.
|
|
75
|
+
// decideApproval returns { decision: 'allow' | 'ask', decidedBy, ... }. Codex has no
|
|
76
|
+
// "park" — for an 'ask' (human-needed) the caller decides whether to hold the response
|
|
77
|
+
// (durable approval) or deny. This pure mapper covers the terminal decisions:
|
|
78
|
+
// allow → v2 'accept' / legacy 'approved'
|
|
79
|
+
// deny → v2 'decline' / legacy 'denied' (continue the turn, try alternative)
|
|
80
|
+
// abort → v2 'cancel' / legacy 'abort' (deny + interrupt the whole turn)
|
|
81
|
+
// `decision` is one of 'allow' | 'deny' | 'abort'.
|
|
82
|
+
function buildCodexApprovalResponse(decision, protocol) {
|
|
83
|
+
const v2 = protocol !== 'legacy';
|
|
84
|
+
const table = v2
|
|
85
|
+
? { allow: 'accept', deny: 'decline', abort: 'cancel' }
|
|
86
|
+
: { allow: 'approved', deny: 'denied', abort: 'abort' };
|
|
87
|
+
const value = table[decision] || (v2 ? 'decline' : 'denied');
|
|
88
|
+
return { decision: value };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Convenience: turn a decideApproval verdict object into the Codex response value.
|
|
92
|
+
// Blocklist/dangerous → 'deny' (Codex has no hard-deny that also refuses to retry; the
|
|
93
|
+
// caller may prefer 'abort' for truly dangerous). Default: allow→accept, else decline.
|
|
94
|
+
function decisionFromVerdict(verdict) {
|
|
95
|
+
if (verdict && verdict.decision === 'allow') return 'allow';
|
|
96
|
+
if (verdict && verdict.decidedBy === 'blocklist') return 'abort';
|
|
97
|
+
return 'deny';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function _ctx(toolName, command, reason) {
|
|
101
|
+
const warning = reason ? String(reason).slice(0, 500) : '';
|
|
102
|
+
return { toolName, command: String(command || '').slice(0, 2000), warning, fullContext: String(command || '').slice(0, 2000) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _joinActions(actions) {
|
|
106
|
+
if (!Array.isArray(actions)) return '';
|
|
107
|
+
return actions.map((a) => (a && (a.command || a.cmd || a.type)) || '').filter(Boolean).join(' ; ').slice(0, 2000);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _grantRootLabel(grantRoot) {
|
|
111
|
+
return grantRoot ? `grant write access under ${grantRoot}` : '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
parseCodexApprovalRequest,
|
|
116
|
+
buildCodexApprovalResponse,
|
|
117
|
+
decisionFromVerdict,
|
|
118
|
+
};
|