@gakr-gakr/codex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autobot.plugin.json +374 -0
- package/doctor-contract-api.ts +68 -0
- package/harness.ts +72 -0
- package/index.ts +124 -0
- package/media-understanding-provider.ts +521 -0
- package/package.json +40 -0
- package/prompt-overlay.ts +21 -0
- package/provider-catalog.ts +83 -0
- package/provider-discovery.ts +45 -0
- package/provider.ts +243 -0
- package/src/app-server/app-inventory-cache.ts +324 -0
- package/src/app-server/approval-bridge.ts +1211 -0
- package/src/app-server/auth-bridge.ts +614 -0
- package/src/app-server/capabilities.ts +27 -0
- package/src/app-server/client-factory.ts +24 -0
- package/src/app-server/client.ts +715 -0
- package/src/app-server/compact.ts +512 -0
- package/src/app-server/computer-use.ts +683 -0
- package/src/app-server/config.ts +1038 -0
- package/src/app-server/context-engine-projection.ts +403 -0
- package/src/app-server/dynamic-tool-diagnostics.ts +73 -0
- package/src/app-server/dynamic-tool-profile.ts +70 -0
- package/src/app-server/dynamic-tools.ts +623 -0
- package/src/app-server/elicitation-bridge.ts +783 -0
- package/src/app-server/event-projector.ts +2065 -0
- package/src/app-server/image-payload-sanitizer.ts +167 -0
- package/src/app-server/local-runtime-attribution.ts +39 -0
- package/src/app-server/managed-binary.ts +193 -0
- package/src/app-server/models.ts +172 -0
- package/src/app-server/native-hook-relay.ts +150 -0
- package/src/app-server/native-subagent-task-mirror.ts +497 -0
- package/src/app-server/plugin-activation.ts +283 -0
- package/src/app-server/plugin-app-cache-key.ts +74 -0
- package/src/app-server/plugin-approval-roundtrip.ts +122 -0
- package/src/app-server/plugin-inventory.ts +357 -0
- package/src/app-server/plugin-thread-config.ts +455 -0
- package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +33 -0
- package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +199 -0
- package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +102 -0
- package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +227 -0
- package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +2630 -0
- package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +2630 -0
- package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +1659 -0
- package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +1655 -0
- package/src/app-server/protocol-validators.ts +203 -0
- package/src/app-server/protocol.ts +520 -0
- package/src/app-server/rate-limit-cache.ts +48 -0
- package/src/app-server/rate-limits.ts +583 -0
- package/src/app-server/request.ts +73 -0
- package/src/app-server/run-attempt.ts +4862 -0
- package/src/app-server/session-binding.ts +398 -0
- package/src/app-server/session-history.ts +44 -0
- package/src/app-server/shared-client.ts +289 -0
- package/src/app-server/side-question.ts +1009 -0
- package/src/app-server/test-support.ts +48 -0
- package/src/app-server/thread-lifecycle.ts +959 -0
- package/src/app-server/timeout.ts +9 -0
- package/src/app-server/tool-progress-normalization.ts +77 -0
- package/src/app-server/trajectory.ts +368 -0
- package/src/app-server/transcript-mirror.ts +208 -0
- package/src/app-server/transport-stdio.ts +107 -0
- package/src/app-server/transport-websocket.ts +90 -0
- package/src/app-server/transport.ts +117 -0
- package/src/app-server/user-input-bridge.ts +316 -0
- package/src/app-server/version.ts +4 -0
- package/src/app-server/vision-tools.ts +12 -0
- package/src/command-account.ts +544 -0
- package/src/command-formatters.ts +426 -0
- package/src/command-handlers.ts +2021 -0
- package/src/command-plugins-management.ts +137 -0
- package/src/command-rpc.ts +142 -0
- package/src/commands.ts +65 -0
- package/src/conversation-binding-data.ts +124 -0
- package/src/conversation-binding.ts +561 -0
- package/src/conversation-control.ts +303 -0
- package/src/conversation-turn-collector.ts +186 -0
- package/src/conversation-turn-input.ts +106 -0
- package/src/migration/apply.ts +501 -0
- package/src/migration/helpers.ts +55 -0
- package/src/migration/plan.ts +461 -0
- package/src/migration/provider.ts +41 -0
- package/src/migration/source.ts +643 -0
- package/src/migration/targets.ts +25 -0
- package/src/node-cli-sessions.ts +711 -0
- package/test-api.ts +95 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { withTimeout as withSharedTimeout } from "autobot/plugin-sdk/security-runtime";
|
|
2
|
+
|
|
3
|
+
export async function withTimeout<T>(
|
|
4
|
+
promise: Promise<T>,
|
|
5
|
+
timeoutMs: number,
|
|
6
|
+
timeoutMessage: string,
|
|
7
|
+
): Promise<T> {
|
|
8
|
+
return await withSharedTimeout(promise, timeoutMs, { message: timeoutMessage });
|
|
9
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
inferToolMetaFromArgs,
|
|
3
|
+
type EmbeddedRunAttemptParams,
|
|
4
|
+
type ToolProgressDetailMode,
|
|
5
|
+
} from "autobot/plugin-sdk/agent-harness-runtime";
|
|
6
|
+
import { redactSensitiveFieldValue, redactToolPayloadText } from "autobot/plugin-sdk/logging-core";
|
|
7
|
+
import {
|
|
8
|
+
isJsonObject,
|
|
9
|
+
type CodexDynamicToolCallParams,
|
|
10
|
+
type CodexDynamicToolCallResponse,
|
|
11
|
+
type JsonValue,
|
|
12
|
+
} from "./protocol.js";
|
|
13
|
+
|
|
14
|
+
export function resolveCodexToolProgressDetailMode(
|
|
15
|
+
value: EmbeddedRunAttemptParams["toolProgressDetail"],
|
|
16
|
+
): ToolProgressDetailMode {
|
|
17
|
+
return value === "raw" ? "raw" : "explain";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sanitizeCodexAgentEventValue(
|
|
21
|
+
value: unknown,
|
|
22
|
+
seen = new WeakSet<object>(),
|
|
23
|
+
): unknown {
|
|
24
|
+
if (typeof value === "string") {
|
|
25
|
+
return redactToolPayloadText(value);
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
if (seen.has(value)) {
|
|
29
|
+
return "[Circular]";
|
|
30
|
+
}
|
|
31
|
+
seen.add(value);
|
|
32
|
+
return value.map((entry) => sanitizeCodexAgentEventValue(entry, seen));
|
|
33
|
+
}
|
|
34
|
+
if (value && typeof value === "object") {
|
|
35
|
+
if (seen.has(value)) {
|
|
36
|
+
return "[Circular]";
|
|
37
|
+
}
|
|
38
|
+
seen.add(value);
|
|
39
|
+
const out: Record<string, unknown> = {};
|
|
40
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
41
|
+
out[key] =
|
|
42
|
+
typeof child === "string"
|
|
43
|
+
? redactSensitiveFieldValue(key, child)
|
|
44
|
+
: sanitizeCodexAgentEventValue(child, seen);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sanitizeCodexAgentEventRecord(
|
|
52
|
+
value: Record<string, unknown>,
|
|
53
|
+
): Record<string, unknown> {
|
|
54
|
+
return sanitizeCodexAgentEventValue(value) as Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sanitizeCodexToolArguments(
|
|
58
|
+
value: JsonValue | undefined,
|
|
59
|
+
): Record<string, unknown> | undefined {
|
|
60
|
+
if (!isJsonObject(value)) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return sanitizeCodexAgentEventRecord(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function sanitizeCodexToolResponse(
|
|
67
|
+
response: CodexDynamicToolCallResponse,
|
|
68
|
+
): Record<string, unknown> {
|
|
69
|
+
return sanitizeCodexAgentEventRecord(response as unknown as Record<string, unknown>);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function inferCodexDynamicToolMeta(
|
|
73
|
+
call: Pick<CodexDynamicToolCallParams, "tool" | "arguments">,
|
|
74
|
+
detailMode: ToolProgressDetailMode,
|
|
75
|
+
): string | undefined {
|
|
76
|
+
return inferToolMetaFromArgs(call.tool, call.arguments, { detailMode });
|
|
77
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import nodeFs from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resolveUserPath } from "autobot/plugin-sdk/agent-harness-runtime";
|
|
5
|
+
import type {
|
|
6
|
+
EmbeddedRunAttemptParams,
|
|
7
|
+
EmbeddedRunAttemptResult,
|
|
8
|
+
} from "autobot/plugin-sdk/agent-harness-runtime";
|
|
9
|
+
import {
|
|
10
|
+
appendRegularFile,
|
|
11
|
+
resolveRegularFileAppendFlags,
|
|
12
|
+
} from "autobot/plugin-sdk/security-runtime";
|
|
13
|
+
import { resolveCodexLocalRuntimeAttribution } from "./local-runtime-attribution.js";
|
|
14
|
+
|
|
15
|
+
export type CodexTrajectoryRecorder = {
|
|
16
|
+
filePath: string;
|
|
17
|
+
recordEvent: (type: string, data?: Record<string, unknown>) => void;
|
|
18
|
+
flush: () => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CodexTrajectoryInit = {
|
|
22
|
+
attempt: EmbeddedRunAttemptParams;
|
|
23
|
+
cwd: string;
|
|
24
|
+
developerInstructions?: string;
|
|
25
|
+
prompt?: string;
|
|
26
|
+
tools?: Array<{ name?: string; description?: string; inputSchema?: unknown }>;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const SENSITIVE_FIELD_RE = /(?:authorization|cookie|credential|key|password|passwd|secret|token)/iu;
|
|
31
|
+
const PRIVATE_PAYLOAD_FIELD_RE = /(?:image|screenshot|attachment|fileData|dataUri)/iu;
|
|
32
|
+
const AUTHORIZATION_VALUE_RE = /\b(Bearer|Basic)\s+[A-Za-z0-9+/._~=-]{8,}/giu;
|
|
33
|
+
const JWT_VALUE_RE = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/gu;
|
|
34
|
+
const COOKIE_PAIR_RE = /\b([A-Za-z][A-Za-z0-9_.-]{1,64})=([A-Za-z0-9+/._~%=-]{16,})(?=;|\s|$)/gu;
|
|
35
|
+
const TRAJECTORY_RUNTIME_FILE_MAX_BYTES = 50 * 1024 * 1024;
|
|
36
|
+
const TRAJECTORY_RUNTIME_EVENT_MAX_BYTES = 256 * 1024;
|
|
37
|
+
|
|
38
|
+
type CodexTrajectoryOpenFlagConstants = Pick<
|
|
39
|
+
typeof nodeFs.constants,
|
|
40
|
+
"O_APPEND" | "O_CREAT" | "O_TRUNC" | "O_WRONLY"
|
|
41
|
+
> &
|
|
42
|
+
Partial<Pick<typeof nodeFs.constants, "O_NOFOLLOW">>;
|
|
43
|
+
|
|
44
|
+
export function resolveCodexTrajectoryAppendFlags(
|
|
45
|
+
constants: CodexTrajectoryOpenFlagConstants = nodeFs.constants,
|
|
46
|
+
): number {
|
|
47
|
+
return resolveRegularFileAppendFlags(constants);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveCodexTrajectoryPointerFlags(
|
|
51
|
+
constants: CodexTrajectoryOpenFlagConstants = nodeFs.constants,
|
|
52
|
+
): number {
|
|
53
|
+
const noFollow = constants.O_NOFOLLOW;
|
|
54
|
+
return (
|
|
55
|
+
constants.O_CREAT |
|
|
56
|
+
constants.O_TRUNC |
|
|
57
|
+
constants.O_WRONLY |
|
|
58
|
+
(typeof noFollow === "number" ? noFollow : 0)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function safeAppendTrajectoryFile(filePath: string, line: string): Promise<void> {
|
|
63
|
+
await appendRegularFile({
|
|
64
|
+
filePath,
|
|
65
|
+
content: line,
|
|
66
|
+
maxFileBytes: TRAJECTORY_RUNTIME_FILE_MAX_BYTES,
|
|
67
|
+
rejectSymlinkParents: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function boundedTrajectoryLine(event: Record<string, unknown>): string | undefined {
|
|
72
|
+
const line = JSON.stringify(event);
|
|
73
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
74
|
+
if (bytes <= TRAJECTORY_RUNTIME_EVENT_MAX_BYTES) {
|
|
75
|
+
return `${line}\n`;
|
|
76
|
+
}
|
|
77
|
+
const truncated = JSON.stringify({
|
|
78
|
+
...event,
|
|
79
|
+
data: {
|
|
80
|
+
truncated: true,
|
|
81
|
+
originalBytes: bytes,
|
|
82
|
+
limitBytes: TRAJECTORY_RUNTIME_EVENT_MAX_BYTES,
|
|
83
|
+
reason: "trajectory-event-size-limit",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
if (Buffer.byteLength(truncated, "utf8") <= TRAJECTORY_RUNTIME_EVENT_MAX_BYTES) {
|
|
87
|
+
return `${truncated}\n`;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveTrajectoryPointerFilePath(sessionFile: string): string {
|
|
93
|
+
return sessionFile.endsWith(".jsonl")
|
|
94
|
+
? `${sessionFile.slice(0, -".jsonl".length)}.trajectory-path.json`
|
|
95
|
+
: `${sessionFile}.trajectory-path.json`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeTrajectoryPointerBestEffort(params: {
|
|
99
|
+
filePath: string;
|
|
100
|
+
sessionFile: string;
|
|
101
|
+
sessionId: string;
|
|
102
|
+
}): void {
|
|
103
|
+
const pointerPath = resolveTrajectoryPointerFilePath(params.sessionFile);
|
|
104
|
+
try {
|
|
105
|
+
const pointerDir = path.resolve(path.dirname(pointerPath));
|
|
106
|
+
if (nodeFs.lstatSync(pointerDir).isSymbolicLink()) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
if (nodeFs.lstatSync(pointerPath).isSymbolicLink()) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const fd = nodeFs.openSync(pointerPath, resolveCodexTrajectoryPointerFlags(), 0o600);
|
|
119
|
+
try {
|
|
120
|
+
nodeFs.writeFileSync(
|
|
121
|
+
fd,
|
|
122
|
+
`${JSON.stringify(
|
|
123
|
+
{
|
|
124
|
+
traceSchema: "autobot-trajectory-pointer",
|
|
125
|
+
schemaVersion: 1,
|
|
126
|
+
sessionId: params.sessionId,
|
|
127
|
+
runtimeFile: params.filePath,
|
|
128
|
+
},
|
|
129
|
+
null,
|
|
130
|
+
2,
|
|
131
|
+
)}\n`,
|
|
132
|
+
"utf8",
|
|
133
|
+
);
|
|
134
|
+
nodeFs.fchmodSync(fd, 0o600);
|
|
135
|
+
} finally {
|
|
136
|
+
nodeFs.closeSync(fd);
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Pointer files are best-effort; the runtime sidecar itself is authoritative.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createCodexTrajectoryRecorder(
|
|
144
|
+
params: CodexTrajectoryInit,
|
|
145
|
+
): CodexTrajectoryRecorder | null {
|
|
146
|
+
const env = params.env ?? process.env;
|
|
147
|
+
const enabled = parseTrajectoryEnabled(env);
|
|
148
|
+
if (!enabled) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const filePath = resolveTrajectoryFilePath({
|
|
153
|
+
env,
|
|
154
|
+
sessionFile: params.attempt.sessionFile,
|
|
155
|
+
sessionId: params.attempt.sessionId,
|
|
156
|
+
});
|
|
157
|
+
const ready = fs
|
|
158
|
+
.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 })
|
|
159
|
+
.catch(() => undefined);
|
|
160
|
+
writeTrajectoryPointerBestEffort({
|
|
161
|
+
filePath,
|
|
162
|
+
sessionFile: params.attempt.sessionFile,
|
|
163
|
+
sessionId: params.attempt.sessionId,
|
|
164
|
+
});
|
|
165
|
+
let queue = Promise.resolve();
|
|
166
|
+
let seq = 0;
|
|
167
|
+
const attribution = resolveCodexLocalRuntimeAttribution(params.attempt);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
filePath,
|
|
171
|
+
recordEvent: (type, data) => {
|
|
172
|
+
const event = {
|
|
173
|
+
traceSchema: "autobot-trajectory",
|
|
174
|
+
schemaVersion: 1,
|
|
175
|
+
traceId: params.attempt.sessionId,
|
|
176
|
+
source: "runtime",
|
|
177
|
+
type,
|
|
178
|
+
ts: new Date().toISOString(),
|
|
179
|
+
seq: (seq += 1),
|
|
180
|
+
sourceSeq: seq,
|
|
181
|
+
sessionId: params.attempt.sessionId,
|
|
182
|
+
sessionKey: params.attempt.sessionKey,
|
|
183
|
+
runId: params.attempt.runId,
|
|
184
|
+
workspaceDir: params.cwd,
|
|
185
|
+
provider: attribution.provider,
|
|
186
|
+
modelId: params.attempt.modelId,
|
|
187
|
+
modelApi: attribution.api,
|
|
188
|
+
data: data ? sanitizeValue(data) : undefined,
|
|
189
|
+
};
|
|
190
|
+
const line = boundedTrajectoryLine(event);
|
|
191
|
+
if (!line) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
queue = queue
|
|
195
|
+
.then(() => ready)
|
|
196
|
+
.then(() => safeAppendTrajectoryFile(filePath, line))
|
|
197
|
+
.catch(() => undefined);
|
|
198
|
+
},
|
|
199
|
+
flush: async () => {
|
|
200
|
+
await queue;
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function recordCodexTrajectoryContext(
|
|
206
|
+
recorder: CodexTrajectoryRecorder | null,
|
|
207
|
+
params: CodexTrajectoryInit,
|
|
208
|
+
): void {
|
|
209
|
+
if (!recorder) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
recorder.recordEvent("context.compiled", {
|
|
213
|
+
systemPrompt: params.developerInstructions,
|
|
214
|
+
prompt: params.prompt ?? params.attempt.prompt,
|
|
215
|
+
imagesCount: params.attempt.images?.length ?? 0,
|
|
216
|
+
tools: toTrajectoryToolDefinitions(params.tools),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function recordCodexTrajectoryCompletion(
|
|
221
|
+
recorder: CodexTrajectoryRecorder | null,
|
|
222
|
+
params: {
|
|
223
|
+
attempt: EmbeddedRunAttemptParams;
|
|
224
|
+
result: EmbeddedRunAttemptResult;
|
|
225
|
+
threadId: string;
|
|
226
|
+
turnId: string;
|
|
227
|
+
timedOut: boolean;
|
|
228
|
+
yieldDetected?: boolean;
|
|
229
|
+
},
|
|
230
|
+
): void {
|
|
231
|
+
if (!recorder) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
recorder.recordEvent("model.completed", {
|
|
235
|
+
threadId: params.threadId,
|
|
236
|
+
turnId: params.turnId,
|
|
237
|
+
timedOut: params.timedOut,
|
|
238
|
+
yieldDetected: params.yieldDetected ?? false,
|
|
239
|
+
aborted: params.result.aborted,
|
|
240
|
+
promptError: normalizeCodexTrajectoryError(params.result.promptError),
|
|
241
|
+
usage: params.result.attemptUsage,
|
|
242
|
+
assistantTexts: params.result.assistantTexts,
|
|
243
|
+
messagesSnapshot: params.result.messagesSnapshot,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function parseTrajectoryEnabled(env: NodeJS.ProcessEnv): boolean {
|
|
248
|
+
const value = env.AUTOBOT_TRAJECTORY?.trim().toLowerCase();
|
|
249
|
+
if (value === "1" || value === "true" || value === "yes" || value === "on") {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (value === "0" || value === "false" || value === "no" || value === "off") {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resolveTrajectoryFilePath(params: {
|
|
259
|
+
env: NodeJS.ProcessEnv;
|
|
260
|
+
sessionFile: string;
|
|
261
|
+
sessionId: string;
|
|
262
|
+
}): string {
|
|
263
|
+
const dirOverride = params.env.AUTOBOT_TRAJECTORY_DIR?.trim();
|
|
264
|
+
if (dirOverride) {
|
|
265
|
+
return resolveContainedPath(
|
|
266
|
+
resolveUserPath(dirOverride),
|
|
267
|
+
`${safeTrajectorySessionFileName(params.sessionId)}.jsonl`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return params.sessionFile.endsWith(".jsonl")
|
|
271
|
+
? `${params.sessionFile.slice(0, -".jsonl".length)}.trajectory.jsonl`
|
|
272
|
+
: `${params.sessionFile}.trajectory.jsonl`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function safeTrajectorySessionFileName(sessionId: string): string {
|
|
276
|
+
const safe = sessionId.replaceAll(/[^A-Za-z0-9_-]/g, "_").slice(0, 120);
|
|
277
|
+
return /[A-Za-z0-9]/u.test(safe) ? safe : "session";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function resolveContainedPath(baseDir: string, fileName: string): string {
|
|
281
|
+
const resolvedBase = path.resolve(baseDir);
|
|
282
|
+
const resolvedFile = path.resolve(resolvedBase, fileName);
|
|
283
|
+
const relative = path.relative(resolvedBase, resolvedFile);
|
|
284
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
285
|
+
throw new Error("Trajectory file path escaped its configured directory");
|
|
286
|
+
}
|
|
287
|
+
return resolvedFile;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function toTrajectoryToolDefinitions(
|
|
291
|
+
tools: Array<{ name?: string; description?: string; inputSchema?: unknown }> | undefined,
|
|
292
|
+
): Array<{ name: string; description?: string; parameters?: unknown }> | undefined {
|
|
293
|
+
if (!tools || tools.length === 0) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
return tools
|
|
297
|
+
.flatMap((tool) => {
|
|
298
|
+
const name = tool.name?.trim();
|
|
299
|
+
if (!name) {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
return [
|
|
303
|
+
{
|
|
304
|
+
name,
|
|
305
|
+
description: tool.description,
|
|
306
|
+
parameters: sanitizeValue(tool.inputSchema),
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
})
|
|
310
|
+
.toSorted((left, right) => left.name.localeCompare(right.name));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function sanitizeValue(value: unknown, depth = 0, key = ""): unknown {
|
|
314
|
+
if (value == null || typeof value === "boolean" || typeof value === "number") {
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
if (typeof value === "string") {
|
|
318
|
+
if (SENSITIVE_FIELD_RE.test(key)) {
|
|
319
|
+
return "<redacted>";
|
|
320
|
+
}
|
|
321
|
+
if (value.startsWith("data:") && value.length > 256) {
|
|
322
|
+
return `<redacted data-uri ${value.slice(0, value.indexOf(",")).length} chars>`;
|
|
323
|
+
}
|
|
324
|
+
if (PRIVATE_PAYLOAD_FIELD_RE.test(key) && value.length > 256) {
|
|
325
|
+
return "<redacted payload>";
|
|
326
|
+
}
|
|
327
|
+
const redacted = redactSensitiveString(value);
|
|
328
|
+
return redacted.length > 20_000 ? `${redacted.slice(0, 20_000)}…` : redacted;
|
|
329
|
+
}
|
|
330
|
+
if (depth >= 6) {
|
|
331
|
+
return "<truncated>";
|
|
332
|
+
}
|
|
333
|
+
if (Array.isArray(value)) {
|
|
334
|
+
return value.slice(0, 100).map((entry) => sanitizeValue(entry, depth + 1, key));
|
|
335
|
+
}
|
|
336
|
+
if (typeof value === "object") {
|
|
337
|
+
const next: Record<string, unknown> = {};
|
|
338
|
+
for (const [key, child] of Object.entries(value).slice(0, 100)) {
|
|
339
|
+
next[key] = sanitizeValue(child, depth + 1, key);
|
|
340
|
+
}
|
|
341
|
+
return next;
|
|
342
|
+
}
|
|
343
|
+
return JSON.stringify(value);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function redactSensitiveString(value: string): string {
|
|
347
|
+
return value
|
|
348
|
+
.replace(AUTHORIZATION_VALUE_RE, "$1 <redacted>")
|
|
349
|
+
.replace(JWT_VALUE_RE, "<redacted-jwt>")
|
|
350
|
+
.replace(COOKIE_PAIR_RE, "$1=<redacted>");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function normalizeCodexTrajectoryError(value: unknown): string | null {
|
|
354
|
+
if (!value) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
if (value instanceof Error) {
|
|
358
|
+
return value.message;
|
|
359
|
+
}
|
|
360
|
+
if (typeof value === "string") {
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
return JSON.stringify(value);
|
|
365
|
+
} catch {
|
|
366
|
+
return "Unknown error";
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
acquireSessionWriteLock,
|
|
5
|
+
appendSessionTranscriptMessage,
|
|
6
|
+
emitSessionTranscriptUpdate,
|
|
7
|
+
resolveSessionWriteLockOptions,
|
|
8
|
+
runAgentHarnessBeforeMessageWriteHook,
|
|
9
|
+
type AgentMessage,
|
|
10
|
+
type EmbeddedRunAttemptParams,
|
|
11
|
+
type SessionWriteLockAcquireTimeoutConfig,
|
|
12
|
+
} from "autobot/plugin-sdk/agent-harness-runtime";
|
|
13
|
+
|
|
14
|
+
type MirroredAgentMessage = Extract<AgentMessage, { role: "user" | "assistant" | "toolResult" }>;
|
|
15
|
+
|
|
16
|
+
const MIRROR_IDENTITY_META_KEY = "mirrorIdentity" as const;
|
|
17
|
+
|
|
18
|
+
function normalizeOptionalString(value: string | null | undefined): string | undefined {
|
|
19
|
+
const normalized = value?.trim();
|
|
20
|
+
return normalized ? normalized : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildSenderLabel(params: {
|
|
24
|
+
senderId?: string;
|
|
25
|
+
senderName?: string;
|
|
26
|
+
senderUsername?: string;
|
|
27
|
+
senderE164?: string;
|
|
28
|
+
}): string | undefined {
|
|
29
|
+
const label = params.senderName ?? params.senderUsername ?? params.senderE164 ?? params.senderId;
|
|
30
|
+
if (!label) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!params.senderId || label.includes(params.senderId)) {
|
|
34
|
+
return label;
|
|
35
|
+
}
|
|
36
|
+
return `${label} (${params.senderId})`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildCodexUserPromptMessage(params: EmbeddedRunAttemptParams): AgentMessage {
|
|
40
|
+
const senderId = normalizeOptionalString(params.senderId);
|
|
41
|
+
const senderName = normalizeOptionalString(params.senderName);
|
|
42
|
+
const senderUsername = normalizeOptionalString(params.senderUsername);
|
|
43
|
+
const senderE164 = normalizeOptionalString(params.senderE164);
|
|
44
|
+
const senderLabel = buildSenderLabel({ senderId, senderName, senderUsername, senderE164 });
|
|
45
|
+
const sourceChannel = normalizeOptionalString(
|
|
46
|
+
params.inputProvenance?.sourceChannel ?? params.messageChannel ?? params.messageProvider,
|
|
47
|
+
);
|
|
48
|
+
return {
|
|
49
|
+
role: "user",
|
|
50
|
+
content: params.prompt,
|
|
51
|
+
timestamp: Date.now(),
|
|
52
|
+
...(params.inputProvenance ? { provenance: params.inputProvenance } : {}),
|
|
53
|
+
...(sourceChannel ? { sourceChannel } : {}),
|
|
54
|
+
...(senderId ? { senderId } : {}),
|
|
55
|
+
...(senderName ? { senderName } : {}),
|
|
56
|
+
...(senderUsername ? { senderUsername } : {}),
|
|
57
|
+
...(senderE164 ? { senderE164 } : {}),
|
|
58
|
+
...(senderLabel ? { senderLabel } : {}),
|
|
59
|
+
} as AgentMessage;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Tag a message with a stable logical identity for mirror dedupe. Callers
|
|
64
|
+
* should use a value that is invariant for the same logical message across
|
|
65
|
+
* re-emits (e.g. `${turnId}:prompt`, `${turnId}:assistant`) but distinct
|
|
66
|
+
* for genuinely-distinct messages (different turns, different kinds). When
|
|
67
|
+
* present this identity replaces the role/content fingerprint in the
|
|
68
|
+
* idempotency key, so the dedupe survives caller-scope rotation without
|
|
69
|
+
* collapsing distinct same-content turns.
|
|
70
|
+
*/
|
|
71
|
+
export function attachCodexMirrorIdentity<T extends AgentMessage>(message: T, identity: string): T {
|
|
72
|
+
const record = message as unknown as Record<string, unknown>;
|
|
73
|
+
const existing = record["__autobot"];
|
|
74
|
+
const baseMeta =
|
|
75
|
+
existing && typeof existing === "object" && !Array.isArray(existing)
|
|
76
|
+
? (existing as Record<string, unknown>)
|
|
77
|
+
: {};
|
|
78
|
+
return {
|
|
79
|
+
...record,
|
|
80
|
+
__autobot: { ...baseMeta, [MIRROR_IDENTITY_META_KEY]: identity },
|
|
81
|
+
} as unknown as T;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readMirrorIdentity(message: MirroredAgentMessage): string | undefined {
|
|
85
|
+
const record = message as unknown as { __autobot?: unknown };
|
|
86
|
+
const meta = record["__autobot"];
|
|
87
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const id = (meta as Record<string, unknown>)[MIRROR_IDENTITY_META_KEY];
|
|
91
|
+
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fallback content fingerprint for callers that did not tag the message
|
|
95
|
+
// with a stable mirror identity. Only role and content participate; volatile
|
|
96
|
+
// metadata (timestamps, usage, etc.) is intentionally excluded so the
|
|
97
|
+
// fingerprint survives snapshot reordering inside a fixed scope. Distinct
|
|
98
|
+
// same-content turns are still distinguished by the caller's idempotency
|
|
99
|
+
// scope when callers route through this fallback.
|
|
100
|
+
function fingerprintMirrorMessageContent(message: MirroredAgentMessage): string {
|
|
101
|
+
const payload = JSON.stringify({ role: message.role, content: message.content });
|
|
102
|
+
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildMirrorDedupeIdentity(message: MirroredAgentMessage): string {
|
|
106
|
+
const explicit = readMirrorIdentity(message);
|
|
107
|
+
if (explicit) {
|
|
108
|
+
return explicit;
|
|
109
|
+
}
|
|
110
|
+
return `${message.role}:${fingerprintMirrorMessageContent(message)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function mirrorCodexAppServerTranscript(params: {
|
|
114
|
+
sessionFile: string;
|
|
115
|
+
sessionKey?: string;
|
|
116
|
+
agentId?: string;
|
|
117
|
+
messages: AgentMessage[];
|
|
118
|
+
idempotencyScope?: string;
|
|
119
|
+
config?: SessionWriteLockAcquireTimeoutConfig;
|
|
120
|
+
}): Promise<void> {
|
|
121
|
+
const messages = params.messages.filter(
|
|
122
|
+
(message): message is MirroredAgentMessage =>
|
|
123
|
+
message.role === "user" || message.role === "assistant" || message.role === "toolResult",
|
|
124
|
+
);
|
|
125
|
+
if (messages.length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const lock = await acquireSessionWriteLock({
|
|
130
|
+
sessionFile: params.sessionFile,
|
|
131
|
+
...resolveSessionWriteLockOptions(params.config),
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
const existingIdempotencyKeys = await readTranscriptIdempotencyKeys(params.sessionFile);
|
|
135
|
+
for (const message of messages) {
|
|
136
|
+
const dedupeIdentity = buildMirrorDedupeIdentity(message);
|
|
137
|
+
const idempotencyKey = params.idempotencyScope
|
|
138
|
+
? `${params.idempotencyScope}:${dedupeIdentity}`
|
|
139
|
+
: undefined;
|
|
140
|
+
if (idempotencyKey && existingIdempotencyKeys.has(idempotencyKey)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const transcriptMessage = {
|
|
144
|
+
...message,
|
|
145
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
146
|
+
} as AgentMessage;
|
|
147
|
+
const nextMessage = runAgentHarnessBeforeMessageWriteHook({
|
|
148
|
+
message: transcriptMessage,
|
|
149
|
+
agentId: params.agentId,
|
|
150
|
+
sessionKey: params.sessionKey,
|
|
151
|
+
});
|
|
152
|
+
if (!nextMessage) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const messageToAppend = (
|
|
156
|
+
idempotencyKey
|
|
157
|
+
? {
|
|
158
|
+
...(nextMessage as unknown as Record<string, unknown>),
|
|
159
|
+
idempotencyKey,
|
|
160
|
+
}
|
|
161
|
+
: nextMessage
|
|
162
|
+
) as AgentMessage;
|
|
163
|
+
await appendSessionTranscriptMessage({
|
|
164
|
+
transcriptPath: params.sessionFile,
|
|
165
|
+
message: messageToAppend,
|
|
166
|
+
config: params.config,
|
|
167
|
+
});
|
|
168
|
+
if (idempotencyKey) {
|
|
169
|
+
existingIdempotencyKeys.add(idempotencyKey);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} finally {
|
|
173
|
+
await lock.release();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (params.sessionKey) {
|
|
177
|
+
emitSessionTranscriptUpdate({ sessionFile: params.sessionFile, sessionKey: params.sessionKey });
|
|
178
|
+
} else {
|
|
179
|
+
emitSessionTranscriptUpdate(params.sessionFile);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function readTranscriptIdempotencyKeys(sessionFile: string): Promise<Set<string>> {
|
|
184
|
+
const keys = new Set<string>();
|
|
185
|
+
let raw: string;
|
|
186
|
+
try {
|
|
187
|
+
raw = await fs.readFile(sessionFile, "utf8");
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
return keys;
|
|
193
|
+
}
|
|
194
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
195
|
+
if (!line.trim()) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(line) as { message?: { idempotencyKey?: unknown } };
|
|
200
|
+
if (typeof parsed.message?.idempotencyKey === "string") {
|
|
201
|
+
keys.add(parsed.message.idempotencyKey);
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return keys;
|
|
208
|
+
}
|