@astrosheep/keiyaku 0.1.87 → 1.0.1
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 +74 -65
- package/build/.tsbuildinfo +1 -1
- package/build/agents/effective-policy.js +32 -0
- package/build/agents/harness/event-channel.js +178 -0
- package/build/agents/harness/execution-handle.js +117 -0
- package/build/agents/harness/index.js +622 -0
- package/build/agents/harness/pump.js +152 -0
- package/build/agents/harness/runtime.js +68 -0
- package/build/agents/index.js +104 -213
- package/build/agents/opencode-sdk.js +12 -18
- package/build/agents/providers/claude-agent-sdk.js +529 -0
- package/build/agents/providers/codex-app-server.js +656 -0
- package/build/agents/providers/codex-sdk.js +193 -0
- package/build/agents/providers/opencode-sdk.js +257 -0
- package/build/agents/providers/pi.js +345 -0
- package/build/agents/selector.js +15 -6
- package/build/agents/session.js +29 -0
- package/build/agents/types.js +5 -11
- package/build/cli/commands/akuma.js +79 -0
- package/build/cli/completion.js +71 -0
- package/build/cli/draft-artifact.js +50 -0
- package/build/cli/flags.js +96 -0
- package/build/cli/help.js +119 -0
- package/build/cli/index.js +204 -912
- package/build/cli/parse.js +280 -0
- package/build/cli/render/arc.js +63 -0
- package/build/{responses/tool-errors.js → cli/render/errors.js} +15 -16
- package/build/cli/render/format.js +199 -0
- package/build/cli/render/misc.js +40 -0
- package/build/cli/render/petition.js +32 -0
- package/build/{responses → cli/render}/response-style.js +4 -4
- package/build/cli/render/shared.js +122 -0
- package/build/cli/render/status.js +69 -0
- package/build/cli/render/summon.js +17 -0
- package/build/cli/render/tell.js +11 -0
- package/build/cli/skills-install.js +237 -0
- package/build/cli/subagent-guard.js +6 -3
- package/build/config/akuma-loader.js +268 -0
- package/build/config/env-keys.js +0 -10
- package/build/config/{schema.js → env.js} +117 -23
- package/build/config/settings.js +137 -260
- package/build/core/amend.js +120 -0
- package/build/core/arc.js +225 -0
- package/build/core/atomic-publish.js +64 -0
- package/build/core/bind.js +281 -0
- package/build/core/claim.js +360 -0
- package/build/core/command-io.js +174 -0
- package/build/core/context.js +108 -0
- package/build/core/contract.js +14 -0
- package/build/core/draft.js +82 -0
- package/build/core/entry.js +203 -0
- package/build/core/forfeit.js +60 -0
- package/build/core/hints.js +108 -0
- package/build/core/ids.js +70 -0
- package/build/core/ledger.js +117 -0
- package/build/core/log.js +24 -0
- package/build/{protocol → core}/markdown/parser.js +68 -0
- package/build/{protocol → core}/markdown/sections.js +2 -28
- package/build/core/markdown/titles.js +9 -0
- package/build/{tools/petition/claim-gates.js → core/petition-claim-gates.js} +43 -29
- package/build/core/petition-claim.js +116 -0
- package/build/core/petition-forfeit.js +39 -0
- package/build/{tools/round/head-guard.js → core/petition-head-guard.js} +2 -2
- package/build/core/petition-run.js +81 -0
- package/build/core/petition.js +96 -0
- package/build/core/places.js +82 -0
- package/build/core/projection-core.js +541 -0
- package/build/core/projection-mint.js +79 -0
- package/build/core/projection-status.js +179 -0
- package/build/core/projection-wake.js +252 -0
- package/build/core/queue.js +73 -0
- package/build/core/ref-log.js +94 -0
- package/build/core/registry.js +96 -0
- package/build/core/render.js +326 -0
- package/build/core/renew.js +195 -0
- package/build/core/scope.js +163 -0
- package/build/core/seal.js +195 -0
- package/build/core/status.js +476 -0
- package/build/{tools/summon/persist.js → core/summon-persist.js} +10 -9
- package/build/core/summon.js +377 -0
- package/build/{protocol/response-history.js → core/transcripts.js} +83 -39
- package/build/core/verdict.js +96 -0
- package/build/core/worktree-path.js +134 -0
- package/build/flow-error.js +80 -4
- package/build/generated/version.js +1 -1
- package/build/git/branches.js +8 -9
- package/build/git/core.js +1 -1
- package/build/git/diff/pathspec.js +2 -3
- package/build/git/diff/preview.js +7 -2
- package/build/git/diff/read.js +4 -3
- package/build/git/staging.js +1 -1
- package/build/index.js +12 -6
- package/build/keiyaku.js +17 -13
- package/build/telemetry/debug-log.js +1 -1
- package/package.json +16 -9
- package/skills/keiyaku/SKILL.md +16 -13
- package/build/agents/claude-agent-sdk-exec.js +0 -352
- package/build/agents/codex-cli-exec.js +0 -237
- package/build/agents/codex-sdk-exec.js +0 -291
- package/build/agents/gemini-cli-core-exec.js +0 -499
- package/build/agents/opencode-exec.js +0 -297
- package/build/agents/process-runner.js +0 -239
- package/build/agents/progress-reporter.js +0 -137
- package/build/agents/round-runner.js +0 -53
- package/build/config/apply-argument-descriptions.js +0 -16
- package/build/config/architect-hints.js +0 -374
- package/build/config/base-rules.js +0 -23
- package/build/config/config.js +0 -50
- package/build/config/dotenv.js +0 -61
- package/build/config/keiyaku-home.js +0 -9
- package/build/config/path-utils.js +0 -11
- package/build/config/render-template.js +0 -8
- package/build/git/diff/filter.js +0 -67
- package/build/git/keiyaku-state.js +0 -58
- package/build/git/snapshot.js +0 -41
- package/build/git/stash.js +0 -62
- package/build/protocol/bind-draft.js +0 -195
- package/build/protocol/draft-artifacts.js +0 -99
- package/build/protocol/file-guards.js +0 -24
- package/build/protocol/keiyaku-draft.js +0 -168
- package/build/protocol/ledger.js +0 -374
- package/build/protocol/markdown/index.js +0 -14
- package/build/protocol/markdown/normalization.js +0 -71
- package/build/protocol/markdown/render.js +0 -68
- package/build/protocol/oath.js +0 -36
- package/build/protocol/render-keiyaku.js +0 -37
- package/build/protocol/review.js +0 -1
- package/build/protocol/status-previews.js +0 -68
- package/build/responses/responses.js +0 -515
- package/build/tools/amend/index.js +0 -18
- package/build/tools/amend/run.js +0 -44
- package/build/tools/bind/index.js +0 -44
- package/build/tools/bind/run.js +0 -237
- package/build/tools/contracts.js +0 -1
- package/build/tools/create-handler.js +0 -31
- package/build/tools/help.js +0 -9
- package/build/tools/petition/claim.js +0 -174
- package/build/tools/petition/forfeit.js +0 -100
- package/build/tools/petition/index.js +0 -83
- package/build/tools/petition/run.js +0 -66
- package/build/tools/round/guidance.js +0 -7
- package/build/tools/round/prompt.js +0 -70
- package/build/tools/round/report.js +0 -153
- package/build/tools/round/worktree.js +0 -55
- package/build/tools/round-close/index.js +0 -19
- package/build/tools/round-close/run.js +0 -98
- package/build/tools/round-open/index.js +0 -20
- package/build/tools/round-open/run.js +0 -77
- package/build/tools/schema.js +0 -69
- package/build/tools/status/index.js +0 -16
- package/build/tools/status/read.js +0 -155
- package/build/tools/summon/index.js +0 -27
- package/build/tools/summon/run.js +0 -80
- /package/build/{config/toml.js → agents/harness/events.js} +0 -0
- /package/build/{git/diff/contracts.js → cli/render/types.js} +0 -0
- /package/build/{protocol/bind-input.js → cli/types.js} +0 -0
- /package/build/{protocol → core}/markdown/lex.js +0 -0
- /package/build/{protocol → core}/markdown/types.js +0 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { appendDebugLog } from "../../telemetry/debug-log.js";
|
|
4
|
+
import { SUBAGENT_SESSION_VERSION } from "../session.js";
|
|
5
|
+
import { createProjectionDriver } from "../harness/execution-handle.js";
|
|
6
|
+
import { buildSubagentEnv, createAbortError, emitProviderEvidence, sha256Utf8 } from "../harness/runtime.js";
|
|
7
|
+
import { coerceString } from "../../agents/index.js";
|
|
8
|
+
/** Session handle for codex-app-server: thread id plus optional CODEX_HOME for cross-process resume. */
|
|
9
|
+
export const codexAppServerSessionSchema = z.object({
|
|
10
|
+
threadId: z.string().trim().min(1),
|
|
11
|
+
codexHome: z.string().trim().min(1).optional(),
|
|
12
|
+
});
|
|
13
|
+
const CODEX_APP_SERVER_PROVIDER = "codex-app-server";
|
|
14
|
+
const MINIMUM_CODEX_APP_SERVER_VERSION = { major: 0, minor: 143, patch: 0 };
|
|
15
|
+
/** Non-interactive defaults for turn/start (faye #316 policy A). */
|
|
16
|
+
export const CODEX_APP_SERVER_TURN_APPROVAL_POLICY = "never";
|
|
17
|
+
export function buildCodexAppServerDefaultSandboxPolicy(cwd) {
|
|
18
|
+
return {
|
|
19
|
+
type: "workspaceWrite",
|
|
20
|
+
writableRoots: [cwd],
|
|
21
|
+
networkAccess: false,
|
|
22
|
+
excludeTmpdirEnvVar: false,
|
|
23
|
+
excludeSlashTmp: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** True when config.* already owns approval policy — omit turn-level approvalPolicy default. */
|
|
27
|
+
export function configOwnsApprovalPolicy(config) {
|
|
28
|
+
if (!config)
|
|
29
|
+
return false;
|
|
30
|
+
return Object.prototype.hasOwnProperty.call(config, "approval_policy");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* True when config.* already owns sandbox settings — omit turn-level sandboxPolicy default.
|
|
34
|
+
* Matches `sandbox_mode` or any `sandbox_workspace_write` / `sandbox_workspace_write.*` key.
|
|
35
|
+
*/
|
|
36
|
+
export function configOwnsSandboxPolicy(config) {
|
|
37
|
+
if (!config)
|
|
38
|
+
return false;
|
|
39
|
+
for (const key of Object.keys(config)) {
|
|
40
|
+
if (key === "sandbox_mode" || key === "sandbox_workspace_write" || key.startsWith("sandbox_workspace_write.")) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
function parseSemver(version) {
|
|
47
|
+
const match = version.trim().match(/(\d+)\.(\d+)\.(\d+)/);
|
|
48
|
+
if (!match)
|
|
49
|
+
return undefined;
|
|
50
|
+
return {
|
|
51
|
+
major: Number(match[1]),
|
|
52
|
+
minor: Number(match[2]),
|
|
53
|
+
patch: Number(match[3]),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function isCodexAppServerVersionBelowMinimum(version) {
|
|
57
|
+
if (!version)
|
|
58
|
+
return false;
|
|
59
|
+
const parsed = parseSemver(version);
|
|
60
|
+
if (!parsed)
|
|
61
|
+
return false;
|
|
62
|
+
if (parsed.major !== MINIMUM_CODEX_APP_SERVER_VERSION.major) {
|
|
63
|
+
return parsed.major < MINIMUM_CODEX_APP_SERVER_VERSION.major;
|
|
64
|
+
}
|
|
65
|
+
if (parsed.minor !== MINIMUM_CODEX_APP_SERVER_VERSION.minor) {
|
|
66
|
+
return parsed.minor < MINIMUM_CODEX_APP_SERVER_VERSION.minor;
|
|
67
|
+
}
|
|
68
|
+
return parsed.patch < MINIMUM_CODEX_APP_SERVER_VERSION.patch;
|
|
69
|
+
}
|
|
70
|
+
function extractVersionFromInitialize(result) {
|
|
71
|
+
if (!result || typeof result !== "object")
|
|
72
|
+
return undefined;
|
|
73
|
+
const record = result;
|
|
74
|
+
const userAgent = coerceString(record.userAgent);
|
|
75
|
+
if (userAgent) {
|
|
76
|
+
const match = userAgent.match(/(\d+\.\d+\.\d+)/);
|
|
77
|
+
if (match)
|
|
78
|
+
return match[1];
|
|
79
|
+
}
|
|
80
|
+
return coerceString(record.version) ?? coerceString(record.cliVersion);
|
|
81
|
+
}
|
|
82
|
+
class CodexAppServerProcess {
|
|
83
|
+
onEvidence;
|
|
84
|
+
onEvidenceDiagnostic;
|
|
85
|
+
child;
|
|
86
|
+
pending = new Map();
|
|
87
|
+
notificationListeners = new Set();
|
|
88
|
+
serverRequestListeners = new Set();
|
|
89
|
+
exitPromise;
|
|
90
|
+
nextId = 1;
|
|
91
|
+
stdoutBuffer = "";
|
|
92
|
+
closed = false;
|
|
93
|
+
exitSettled = false;
|
|
94
|
+
codexHome;
|
|
95
|
+
constructor(executable, env, codexHome, onEvidence, onEvidenceDiagnostic) {
|
|
96
|
+
this.onEvidence = onEvidence;
|
|
97
|
+
this.onEvidenceDiagnostic = onEvidenceDiagnostic;
|
|
98
|
+
this.codexHome = codexHome;
|
|
99
|
+
const spawnEnv = {
|
|
100
|
+
...env,
|
|
101
|
+
...(codexHome ? { CODEX_HOME: codexHome } : {}),
|
|
102
|
+
};
|
|
103
|
+
this.child = spawn(executable, ["app-server", "--listen", "stdio://"], {
|
|
104
|
+
env: spawnEnv,
|
|
105
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
106
|
+
});
|
|
107
|
+
if (this.onEvidence && this.child.pid)
|
|
108
|
+
emitProviderEvidence(this.onEvidence, { type: "process", provider: CODEX_APP_SERVER_PROVIDER, event: "spawn", pid: this.child.pid }, this.onEvidenceDiagnostic);
|
|
109
|
+
this.child.stdout.setEncoding("utf8");
|
|
110
|
+
this.child.stderr.setEncoding("utf8");
|
|
111
|
+
this.child.stdout.on("data", (chunk) => this.onStdout(chunk));
|
|
112
|
+
// Preserve the pre-evidence diagnostic stream exactly. Process evidence
|
|
113
|
+
// never retains stderr bytes, avoiding credential boundary ambiguity.
|
|
114
|
+
this.child.stderr.on("data", (chunk) => this.onEvidenceDiagnostic?.(chunk));
|
|
115
|
+
this.child.on("error", (error) => this.failAll(error));
|
|
116
|
+
this.exitPromise = new Promise((resolve) => {
|
|
117
|
+
this.child.once("close", (exitCode) => {
|
|
118
|
+
this.exitSettled = true;
|
|
119
|
+
this.closed = true;
|
|
120
|
+
// With no observer, retain the historical error bytes. Evidence adds
|
|
121
|
+
// structured exit facts without changing provider behavior.
|
|
122
|
+
this.failAll(new Error(`${CODEX_APP_SERVER_PROVIDER} process closed`));
|
|
123
|
+
if (this.onEvidence)
|
|
124
|
+
emitProviderEvidence(this.onEvidence, { type: "process", provider: CODEX_APP_SERVER_PROVIDER, event: "exit", exitCode, stderrTail: "" }, this.onEvidenceDiagnostic);
|
|
125
|
+
resolve();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
onNotification(listener) {
|
|
130
|
+
this.notificationListeners.add(listener);
|
|
131
|
+
return () => this.notificationListeners.delete(listener);
|
|
132
|
+
}
|
|
133
|
+
onServerRequest(listener) {
|
|
134
|
+
this.serverRequestListeners.add(listener);
|
|
135
|
+
return () => this.serverRequestListeners.delete(listener);
|
|
136
|
+
}
|
|
137
|
+
onStdout(chunk) {
|
|
138
|
+
this.stdoutBuffer += chunk;
|
|
139
|
+
let newline = this.stdoutBuffer.indexOf("\n");
|
|
140
|
+
while (newline >= 0) {
|
|
141
|
+
const line = this.stdoutBuffer.slice(0, newline).trim();
|
|
142
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newline + 1);
|
|
143
|
+
if (line.length > 0) {
|
|
144
|
+
this.handleLine(line);
|
|
145
|
+
}
|
|
146
|
+
newline = this.stdoutBuffer.indexOf("\n");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
handleLine(line) {
|
|
150
|
+
let message;
|
|
151
|
+
try {
|
|
152
|
+
message = JSON.parse(line);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!message || typeof message !== "object")
|
|
158
|
+
return;
|
|
159
|
+
const record = message;
|
|
160
|
+
if ("id" in record && ("result" in record || "error" in record)) {
|
|
161
|
+
const id = record.id;
|
|
162
|
+
const pending = this.pending.get(id);
|
|
163
|
+
if (!pending)
|
|
164
|
+
return;
|
|
165
|
+
this.pending.delete(id);
|
|
166
|
+
if ("error" in record && record.error) {
|
|
167
|
+
const error = record.error;
|
|
168
|
+
pending.reject(new Error(error.message ?? `${CODEX_APP_SERVER_PROVIDER} RPC error`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
pending.resolve(record.result);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Server→client request: method + id. Never ignore, auto-approve, or auto-deny (faye #316).
|
|
175
|
+
if (typeof record.method === "string" && "id" in record) {
|
|
176
|
+
const request = {
|
|
177
|
+
id: record.id,
|
|
178
|
+
method: record.method,
|
|
179
|
+
params: record.params ?? undefined,
|
|
180
|
+
};
|
|
181
|
+
for (const listener of this.serverRequestListeners) {
|
|
182
|
+
listener(request);
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (typeof record.method === "string" && !("id" in record)) {
|
|
187
|
+
const notification = {
|
|
188
|
+
method: record.method,
|
|
189
|
+
params: record.params ?? undefined,
|
|
190
|
+
};
|
|
191
|
+
for (const listener of this.notificationListeners) {
|
|
192
|
+
listener(notification);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
failAll(error) {
|
|
197
|
+
for (const pending of this.pending.values()) {
|
|
198
|
+
pending.reject(error);
|
|
199
|
+
}
|
|
200
|
+
this.pending.clear();
|
|
201
|
+
}
|
|
202
|
+
request(method, params) {
|
|
203
|
+
if (this.closed) {
|
|
204
|
+
return Promise.reject(new Error(`${CODEX_APP_SERVER_PROVIDER} process is closed`));
|
|
205
|
+
}
|
|
206
|
+
const id = this.nextId++;
|
|
207
|
+
const payload = params === undefined ? { id, method } : { id, method, params };
|
|
208
|
+
const serialized = `${JSON.stringify(payload)}\n`;
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
this.pending.set(id, { resolve, reject });
|
|
211
|
+
this.child.stdin.write(serialized, (error) => {
|
|
212
|
+
if (error) {
|
|
213
|
+
this.pending.delete(id);
|
|
214
|
+
reject(error);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// The write call above is the process boundary. Emitting afterward
|
|
218
|
+
// avoids evidence for a synchronous write failure while keeping the
|
|
219
|
+
// outbound fact before any asynchronous RPC acknowledgement.
|
|
220
|
+
this.emitOutbound(method, params);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
notify(method, params) {
|
|
224
|
+
const payload = params === undefined ? { method } : { method, params };
|
|
225
|
+
this.child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
226
|
+
}
|
|
227
|
+
emitOutbound(method, params) {
|
|
228
|
+
if (!this.onEvidence)
|
|
229
|
+
return;
|
|
230
|
+
const kind = method === "thread/start" ? "thread-start" : method === "thread/resume" ? "thread-resume" : method === "turn/start" ? "turn-start" : method === "turn/steer" ? "steer" : undefined;
|
|
231
|
+
if (!kind)
|
|
232
|
+
return;
|
|
233
|
+
const text = method === "turn/steer"
|
|
234
|
+
? coerceString((params?.input?.[0]?.text))
|
|
235
|
+
: JSON.stringify(params ?? {});
|
|
236
|
+
if (text === undefined)
|
|
237
|
+
return;
|
|
238
|
+
emitProviderEvidence(this.onEvidence, { type: "outbound", provider: CODEX_APP_SERVER_PROVIDER, direction: "request", kind, ...sha256Utf8(text) }, this.onEvidenceDiagnostic);
|
|
239
|
+
const developerInstructions = coerceString(params?.developerInstructions);
|
|
240
|
+
if ((method === "thread/start" || method === "thread/resume") && developerInstructions !== undefined) {
|
|
241
|
+
emitProviderEvidence(this.onEvidence, { type: "outbound", provider: CODEX_APP_SERVER_PROVIDER, direction: "request", kind: "system-prompt", ...sha256Utf8(developerInstructions) }, this.onEvidenceDiagnostic);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async close() {
|
|
245
|
+
if (this.exitSettled) {
|
|
246
|
+
this.closed = true;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.closed = true;
|
|
250
|
+
this.failAll(new Error(`${CODEX_APP_SERVER_PROVIDER} process closed`));
|
|
251
|
+
try {
|
|
252
|
+
this.child.stdin.end();
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// ignore
|
|
256
|
+
}
|
|
257
|
+
// Destroy stdio so a hung child cannot keep the parent event loop alive via open pipes.
|
|
258
|
+
try {
|
|
259
|
+
this.child.stdout.destroy();
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// ignore
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
this.child.stderr.destroy();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// ignore
|
|
269
|
+
}
|
|
270
|
+
if (!this.exitSettled && this.child.exitCode === null && this.child.signalCode === null) {
|
|
271
|
+
try {
|
|
272
|
+
this.child.kill("SIGTERM");
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// ignore
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Escalate to SIGKILL if the child ignores stdin EOF + SIGTERM, then wait for exit
|
|
279
|
+
// (or give up after a hard bound so close() itself cannot pin the event loop).
|
|
280
|
+
const forceKill = new Promise((resolve) => {
|
|
281
|
+
const timer = setTimeout(() => {
|
|
282
|
+
if (!this.exitSettled) {
|
|
283
|
+
try {
|
|
284
|
+
this.child.kill("SIGKILL");
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// ignore
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
resolve();
|
|
291
|
+
}, 1000);
|
|
292
|
+
timer.unref?.();
|
|
293
|
+
});
|
|
294
|
+
const hardBound = new Promise((resolve) => {
|
|
295
|
+
const timer = setTimeout(resolve, 2000);
|
|
296
|
+
timer.unref?.();
|
|
297
|
+
});
|
|
298
|
+
await Promise.race([this.exitPromise, Promise.all([forceKill, this.exitPromise]).then(() => undefined), hardBound]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** Gate-facing production entry: the harness owns clocks, capture, classification, and outcome construction. */
|
|
302
|
+
export async function startCodexAppServer(prompt, cwd, options = {}) {
|
|
303
|
+
const executable = options.executable?.trim() || "codex";
|
|
304
|
+
const resumeThreadId = options.threadId?.trim();
|
|
305
|
+
let server;
|
|
306
|
+
let activeTurnId;
|
|
307
|
+
let activeTurnSource;
|
|
308
|
+
let activeTurnAcceptingSteer = false;
|
|
309
|
+
let activeThreadId;
|
|
310
|
+
let interruptRequested = false;
|
|
311
|
+
let lastAgentMessage = "";
|
|
312
|
+
let builderDiagnostic;
|
|
313
|
+
const pendingTells = [];
|
|
314
|
+
let drainingTells = false;
|
|
315
|
+
let failActiveTurn;
|
|
316
|
+
const rejectPendingTells = (error) => {
|
|
317
|
+
for (const pending of pendingTells.splice(0))
|
|
318
|
+
pending.reject(error);
|
|
319
|
+
};
|
|
320
|
+
const recordActiveTurnId = (candidate, source) => {
|
|
321
|
+
const turnId = coerceString(candidate);
|
|
322
|
+
if (!turnId)
|
|
323
|
+
throw new Error(`${CODEX_APP_SERVER_PROVIDER} ${source} omitted active turn id`);
|
|
324
|
+
if (!activeTurnId) {
|
|
325
|
+
activeTurnId = turnId;
|
|
326
|
+
activeTurnSource = source;
|
|
327
|
+
return turnId;
|
|
328
|
+
}
|
|
329
|
+
if (activeTurnId !== turnId) {
|
|
330
|
+
throw new Error(`${CODEX_APP_SERVER_PROVIDER} active turn id mismatch: ${activeTurnSource ?? "first"}=${activeTurnId}, ${source}=${turnId}`);
|
|
331
|
+
}
|
|
332
|
+
return activeTurnId;
|
|
333
|
+
};
|
|
334
|
+
appendDebugLog(`startCodexAppServer: executable=${executable} model=${options.model ?? "default"} mode=${resumeThreadId ? "resume" : "fresh"}`, { cwd, section: CODEX_APP_SERVER_PROVIDER });
|
|
335
|
+
const submitSteer = async (tell, turnId) => {
|
|
336
|
+
// turn/steer response {turnId} = enqueue acceptance (submitted), not consumption.
|
|
337
|
+
// clientUserMessageId = tell.id for later delivered proof via userMessage item clientId.
|
|
338
|
+
// tell.text is already core-rendered; send byte-for-byte — never parse or rewrite.
|
|
339
|
+
const ack = await server.request("turn/steer", {
|
|
340
|
+
threadId: activeThreadId,
|
|
341
|
+
expectedTurnId: turnId,
|
|
342
|
+
clientUserMessageId: tell.id,
|
|
343
|
+
input: [{ type: "text", text: tell.text }],
|
|
344
|
+
});
|
|
345
|
+
const acknowledgedTurnId = coerceString(ack.turnId) ?? turnId;
|
|
346
|
+
if (acknowledgedTurnId !== turnId) {
|
|
347
|
+
throw new Error(`${CODEX_APP_SERVER_PROVIDER} turn/steer acknowledgement mismatched active turn: expected=${turnId}, actual=${acknowledgedTurnId}`);
|
|
348
|
+
}
|
|
349
|
+
if (options.onEvidence) {
|
|
350
|
+
emitProviderEvidence(options.onEvidence, {
|
|
351
|
+
type: "ack",
|
|
352
|
+
provider: CODEX_APP_SERVER_PROVIDER,
|
|
353
|
+
fence: { kind: "turn", turnId: acknowledgedTurnId },
|
|
354
|
+
outbound: "steer",
|
|
355
|
+
}, builderDiagnostic);
|
|
356
|
+
}
|
|
357
|
+
return { fence: { kind: "turn", turnId: acknowledgedTurnId } };
|
|
358
|
+
};
|
|
359
|
+
const startTellTurn = async (tell) => {
|
|
360
|
+
const started = await server.request("turn/start", {
|
|
361
|
+
threadId: activeThreadId,
|
|
362
|
+
input: [{ type: "text", text: tell.text }],
|
|
363
|
+
...(options.model ? { model: options.model } : {}),
|
|
364
|
+
...(tell.effort ? { effort: tell.effort } : {}),
|
|
365
|
+
...(!configOwnsApprovalPolicy(options.config) ? { approvalPolicy: CODEX_APP_SERVER_TURN_APPROVAL_POLICY } : {}),
|
|
366
|
+
...(!configOwnsSandboxPolicy(options.config) ? { sandboxPolicy: buildCodexAppServerDefaultSandboxPolicy(cwd) } : {}),
|
|
367
|
+
});
|
|
368
|
+
const startedTurn = started && typeof started === "object"
|
|
369
|
+
? (started.turn ?? undefined)
|
|
370
|
+
: undefined;
|
|
371
|
+
const newTurnId = recordActiveTurnId(startedTurn?.id, "response");
|
|
372
|
+
if (options.onEvidence)
|
|
373
|
+
emitProviderEvidence(options.onEvidence, { type: "ack", provider: CODEX_APP_SERVER_PROVIDER, fence: { kind: "turn", turnId: newTurnId }, outbound: "run" }, builderDiagnostic);
|
|
374
|
+
return { fence: { kind: "turn", turnId: newTurnId } };
|
|
375
|
+
};
|
|
376
|
+
const drainPendingTells = async () => {
|
|
377
|
+
if (drainingTells)
|
|
378
|
+
return;
|
|
379
|
+
drainingTells = true;
|
|
380
|
+
try {
|
|
381
|
+
while (pendingTells.length > 0 && server && activeThreadId) {
|
|
382
|
+
const pending = pendingTells[0];
|
|
383
|
+
if (pending.tell.effort && activeTurnId)
|
|
384
|
+
return;
|
|
385
|
+
if (!pending.tell.effort && activeTurnId && !activeTurnAcceptingSteer)
|
|
386
|
+
return;
|
|
387
|
+
pendingTells.shift();
|
|
388
|
+
try {
|
|
389
|
+
const ack = activeTurnId
|
|
390
|
+
? await submitSteer(pending.tell, activeTurnId)
|
|
391
|
+
: await startTellTurn(pending.tell);
|
|
392
|
+
pending.resolve(ack);
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
pending.reject(error);
|
|
396
|
+
failActiveTurn?.(error);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
drainingTells = false;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const enqueue = async (tell) => {
|
|
406
|
+
if (!server || !activeThreadId) {
|
|
407
|
+
throw new Error(`${CODEX_APP_SERVER_PROVIDER} cannot steer: no active turn`);
|
|
408
|
+
}
|
|
409
|
+
return await new Promise((resolve, reject) => {
|
|
410
|
+
pendingTells.push({ tell, resolve, reject });
|
|
411
|
+
void drainPendingTells();
|
|
412
|
+
});
|
|
413
|
+
};
|
|
414
|
+
return createProjectionDriver({
|
|
415
|
+
steerMode: "mid-execution",
|
|
416
|
+
enqueue,
|
|
417
|
+
async abort() {
|
|
418
|
+
interruptRequested = true;
|
|
419
|
+
if (server && activeThreadId && activeTurnId) {
|
|
420
|
+
try {
|
|
421
|
+
// RPC acknowledgement is NOT terminal proof; wait for turn/completed interrupted.
|
|
422
|
+
await server.request("turn/interrupt", {
|
|
423
|
+
threadId: activeThreadId,
|
|
424
|
+
turnId: activeTurnId,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// idle interrupt may error; still rely on process teardown
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
await server?.close();
|
|
432
|
+
},
|
|
433
|
+
async run(builder) {
|
|
434
|
+
builderDiagnostic = (text) => builder.emit({ type: "diagnostic", text });
|
|
435
|
+
const env = buildSubagentEnv();
|
|
436
|
+
server = new CodexAppServerProcess(executable, env, options.codexHome, options.onEvidence, (text) => builder.emit({ type: "diagnostic", text }));
|
|
437
|
+
const turnCompleted = new Promise((resolve, reject) => {
|
|
438
|
+
let unsubNotifications = () => { };
|
|
439
|
+
let unsubServerRequest = () => { };
|
|
440
|
+
let settled = false;
|
|
441
|
+
const failTurn = (error) => {
|
|
442
|
+
if (settled)
|
|
443
|
+
return;
|
|
444
|
+
settled = true;
|
|
445
|
+
rejectPendingTells(error);
|
|
446
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
447
|
+
builder.emit({ type: "diagnostic", text: message });
|
|
448
|
+
builder.emit({ type: "fail", message });
|
|
449
|
+
unsubNotifications();
|
|
450
|
+
unsubServerRequest();
|
|
451
|
+
reject(error);
|
|
452
|
+
};
|
|
453
|
+
const failServerRequest = (request) => {
|
|
454
|
+
// Policy A forbids interactive server→client requests. Fail immediately with named diagnostic.
|
|
455
|
+
const message = `${CODEX_APP_SERVER_PROVIDER} unexpected server request '${request.method}' (id=${String(request.id)}); non-interactive policy forbids server→client requests`;
|
|
456
|
+
failTurn(new Error(message));
|
|
457
|
+
};
|
|
458
|
+
unsubServerRequest = server.onServerRequest((request) => {
|
|
459
|
+
failServerRequest(request);
|
|
460
|
+
});
|
|
461
|
+
unsubNotifications = server.onNotification((notification) => {
|
|
462
|
+
try {
|
|
463
|
+
builder.emit({ type: "output", value: notification });
|
|
464
|
+
if (notification.method === "item/agentMessage/delta") {
|
|
465
|
+
const delta = coerceString(notification.params?.delta);
|
|
466
|
+
if (delta) {
|
|
467
|
+
lastAgentMessage = `${lastAgentMessage}${delta}`;
|
|
468
|
+
builder.emit({ type: "message", text: lastAgentMessage });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (notification.method === "item/completed") {
|
|
472
|
+
const item = notification.params?.item;
|
|
473
|
+
if (item && (item.type === "agentMessage" || item.type === "agent_message")) {
|
|
474
|
+
const text = coerceString(item.text);
|
|
475
|
+
if (text) {
|
|
476
|
+
lastAgentMessage = text;
|
|
477
|
+
builder.emit({ type: "message", text });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (notification.method === "turn/started") {
|
|
482
|
+
const turn = notification.params?.turn;
|
|
483
|
+
const turnId = recordActiveTurnId(turn?.id, "notification");
|
|
484
|
+
activeTurnAcceptingSteer = true;
|
|
485
|
+
const checkpoint = { fence: { kind: "turn", turnId } };
|
|
486
|
+
if (options.onEvidence) {
|
|
487
|
+
emitProviderEvidence(options.onEvidence, {
|
|
488
|
+
type: "checkpoint",
|
|
489
|
+
provider: CODEX_APP_SERVER_PROVIDER,
|
|
490
|
+
...checkpoint,
|
|
491
|
+
source: "notification",
|
|
492
|
+
}, builderDiagnostic);
|
|
493
|
+
}
|
|
494
|
+
// turn/start may respond before the server has installed its active
|
|
495
|
+
// turn. The native notification is the first steer-safe boundary.
|
|
496
|
+
options.onSyncCheckpoint?.({ enqueue }, checkpoint);
|
|
497
|
+
void drainPendingTells();
|
|
498
|
+
}
|
|
499
|
+
if (notification.method === "turn/completed") {
|
|
500
|
+
const turn = notification.params?.turn ?? {};
|
|
501
|
+
const status = coerceString(turn.status) ?? "unknown";
|
|
502
|
+
// Either response or notification may mint the turn fence first;
|
|
503
|
+
// every later native source must agree with it.
|
|
504
|
+
const turnId = recordActiveTurnId(turn.id, "notification");
|
|
505
|
+
// This must stay in the native notification call stack: the harness
|
|
506
|
+
// checkpoint and its observer evidence describe this exact crossing.
|
|
507
|
+
if (status === "completed") {
|
|
508
|
+
activeTurnAcceptingSteer = false;
|
|
509
|
+
activeTurnId = undefined;
|
|
510
|
+
activeTurnSource = undefined;
|
|
511
|
+
const checkpoint = { fence: { kind: "turn", turnId } };
|
|
512
|
+
if (options.onEvidence) {
|
|
513
|
+
emitProviderEvidence(options.onEvidence, {
|
|
514
|
+
type: "checkpoint",
|
|
515
|
+
provider: CODEX_APP_SERVER_PROVIDER,
|
|
516
|
+
...checkpoint,
|
|
517
|
+
source: "notification",
|
|
518
|
+
}, builderDiagnostic);
|
|
519
|
+
}
|
|
520
|
+
options.onSyncCheckpoint?.({ enqueue }, checkpoint);
|
|
521
|
+
void drainPendingTells();
|
|
522
|
+
if (pendingTells.length > 0 || drainingTells) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (settled)
|
|
527
|
+
return;
|
|
528
|
+
settled = true;
|
|
529
|
+
unsubNotifications();
|
|
530
|
+
unsubServerRequest();
|
|
531
|
+
resolve({ status, turn });
|
|
532
|
+
}
|
|
533
|
+
if (notification.method === "error") {
|
|
534
|
+
const message = coerceString(notification.params?.message) ?? `${CODEX_APP_SERVER_PROVIDER} error`;
|
|
535
|
+
builder.emit({ type: "diagnostic", text: message });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
// Notification listeners run outside the driver's await chain;
|
|
540
|
+
// convert validation failures into the controlled completion path.
|
|
541
|
+
failTurn(error);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
failActiveTurn = failTurn;
|
|
545
|
+
builder.signal.addEventListener("abort", () => {
|
|
546
|
+
unsubNotifications();
|
|
547
|
+
unsubServerRequest();
|
|
548
|
+
failTurn(createAbortError(`${CODEX_APP_SERVER_PROVIDER} exec cancelled by client`));
|
|
549
|
+
}, { once: true });
|
|
550
|
+
});
|
|
551
|
+
// Abort can settle this before initialization reaches the await below.
|
|
552
|
+
// Attach a handler immediately while preserving the original awaited error.
|
|
553
|
+
turnCompleted.catch(() => undefined);
|
|
554
|
+
try {
|
|
555
|
+
const initResult = await server.request("initialize", {
|
|
556
|
+
clientInfo: {
|
|
557
|
+
name: "keiyaku",
|
|
558
|
+
title: "keiyaku",
|
|
559
|
+
version: "0.0.0",
|
|
560
|
+
},
|
|
561
|
+
capabilities: {
|
|
562
|
+
experimentalApi: false,
|
|
563
|
+
requestAttestation: false,
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
const version = extractVersionFromInitialize(initResult);
|
|
567
|
+
if (isCodexAppServerVersionBelowMinimum(version)) {
|
|
568
|
+
const message = `${CODEX_APP_SERVER_PROVIDER} refused: initialize version '${version}' is below minimum supported version 0.143.0`;
|
|
569
|
+
builder.emit({ type: "fail", message });
|
|
570
|
+
throw new Error(message);
|
|
571
|
+
}
|
|
572
|
+
const initRecord = (initResult && typeof initResult === "object" ? initResult : {});
|
|
573
|
+
const serverCodexHome = coerceString(initRecord.codexHome) ?? options.codexHome;
|
|
574
|
+
server.notify("initialized");
|
|
575
|
+
const threadParams = {
|
|
576
|
+
cwd,
|
|
577
|
+
...(options.model ? { model: options.model } : {}),
|
|
578
|
+
...(options.config ? { config: options.config } : {}),
|
|
579
|
+
...(options.developerInstructions ? { developerInstructions: options.developerInstructions } : {}),
|
|
580
|
+
};
|
|
581
|
+
const threadResult = resumeThreadId
|
|
582
|
+
? await server.request("thread/resume", {
|
|
583
|
+
threadId: resumeThreadId,
|
|
584
|
+
...threadParams,
|
|
585
|
+
})
|
|
586
|
+
: await server.request("thread/start", threadParams);
|
|
587
|
+
const thread = threadResult && typeof threadResult === "object"
|
|
588
|
+
? (threadResult.thread ??
|
|
589
|
+
threadResult)
|
|
590
|
+
: undefined;
|
|
591
|
+
activeThreadId = coerceString(thread?.id) ?? resumeThreadId;
|
|
592
|
+
if (!activeThreadId) {
|
|
593
|
+
const message = `${CODEX_APP_SERVER_PROVIDER} exec could not determine thread id`;
|
|
594
|
+
builder.emit({ type: "fail", message });
|
|
595
|
+
throw new Error(message);
|
|
596
|
+
}
|
|
597
|
+
if (options.onEvidence)
|
|
598
|
+
emitProviderEvidence(options.onEvidence, { type: "session", provider: CODEX_APP_SERVER_PROVIDER, threadId: activeThreadId }, builderDiagnostic);
|
|
599
|
+
// Policy A defaults, but omit when config.* already owns that surface (thread/start config wins).
|
|
600
|
+
const turnStartParams = {
|
|
601
|
+
threadId: activeThreadId,
|
|
602
|
+
input: [{ type: "text", text: prompt }],
|
|
603
|
+
...(options.model ? { model: options.model } : {}),
|
|
604
|
+
...(options.effort ? { effort: options.effort } : {}),
|
|
605
|
+
};
|
|
606
|
+
if (!configOwnsApprovalPolicy(options.config)) {
|
|
607
|
+
turnStartParams.approvalPolicy = CODEX_APP_SERVER_TURN_APPROVAL_POLICY;
|
|
608
|
+
}
|
|
609
|
+
if (!configOwnsSandboxPolicy(options.config)) {
|
|
610
|
+
turnStartParams.sandboxPolicy = buildCodexAppServerDefaultSandboxPolicy(cwd);
|
|
611
|
+
}
|
|
612
|
+
activeTurnId = undefined;
|
|
613
|
+
activeTurnSource = undefined;
|
|
614
|
+
activeTurnAcceptingSteer = false;
|
|
615
|
+
const turnStart = await server.request("turn/start", turnStartParams);
|
|
616
|
+
const turn = turnStart && typeof turnStart === "object"
|
|
617
|
+
? (turnStart.turn ?? undefined)
|
|
618
|
+
: undefined;
|
|
619
|
+
recordActiveTurnId(turn?.id, "response");
|
|
620
|
+
if (options.onEvidence)
|
|
621
|
+
emitProviderEvidence(options.onEvidence, { type: "checkpoint", provider: CODEX_APP_SERVER_PROVIDER, fence: { kind: "turn", turnId: activeTurnId }, source: "response" }, builderDiagnostic);
|
|
622
|
+
const completed = await turnCompleted;
|
|
623
|
+
if (completed.status === "interrupted" || interruptRequested) {
|
|
624
|
+
throw createAbortError(`${CODEX_APP_SERVER_PROVIDER} turn interrupted`);
|
|
625
|
+
}
|
|
626
|
+
if (completed.status === "failed") {
|
|
627
|
+
const message = `${CODEX_APP_SERVER_PROVIDER} turn failed`;
|
|
628
|
+
builder.emit({ type: "fail", message });
|
|
629
|
+
throw new Error(message);
|
|
630
|
+
}
|
|
631
|
+
if (completed.status !== "completed") {
|
|
632
|
+
const message = `${CODEX_APP_SERVER_PROVIDER} turn ended with status '${completed.status}'`;
|
|
633
|
+
builder.emit({ type: "fail", message });
|
|
634
|
+
throw new Error(message);
|
|
635
|
+
}
|
|
636
|
+
// Successful turn/completed is consumption evidence; interrupt ack alone is not terminal proof.
|
|
637
|
+
const session = {
|
|
638
|
+
provider: "codex-app-server",
|
|
639
|
+
version: SUBAGENT_SESSION_VERSION,
|
|
640
|
+
handle: {
|
|
641
|
+
threadId: activeThreadId,
|
|
642
|
+
...(serverCodexHome ? { codexHome: serverCodexHome } : {}),
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
const finalMessage = lastAgentMessage;
|
|
646
|
+
builder.emit({ type: "done", finalMessage, session });
|
|
647
|
+
return { finalMessage, session };
|
|
648
|
+
}
|
|
649
|
+
finally {
|
|
650
|
+
rejectPendingTells(new Error(`${CODEX_APP_SERVER_PROVIDER} execution ended before queued tell acceptance`));
|
|
651
|
+
await server?.close();
|
|
652
|
+
server = undefined;
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
}
|