@xfey/tutti 0.1.108 → 0.1.109
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 +2 -0
- package/dist/chat-assistant/diagnostics.d.ts +17 -0
- package/dist/chat-assistant/diagnostics.js +27 -0
- package/dist/chat-assistant/index.d.ts +4 -0
- package/dist/chat-assistant/index.js +30 -7
- package/dist/providers/invocation-progress.d.ts +10 -0
- package/dist/providers/invocation-progress.js +9 -0
- package/dist/providers/openai/app-server/read-only-procedure.d.ts +2 -0
- package/dist/providers/openai/app-server/read-only-procedure.js +31 -2
- package/dist/providers/openai/app-server/skill-discovery.d.ts +10 -0
- package/dist/providers/openai/app-server/skill-discovery.js +29 -0
- package/dist/providers/openai/app-server/skills.d.ts +4 -16
- package/dist/providers/openai/app-server/skills.js +4 -29
- package/dist/providers/openai/app-server/workspace-write-run.js +13 -19
- package/dist/providers/openai/chat-assistant.js +12 -2
- package/dist/server-shell/cli/host-server-runtime.js +2 -0
- package/dist/server-shell/http/routes/agent-context-external-reference-routes.js +29 -4
- package/dist/server-shell/http/static-web.js +10 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
`apps/server` 是 Tutti 的项目 host service,也是机器级 Local Console 的进程入口。
|
|
6
6
|
|
|
7
|
+
`0.1.109` 的 Codex skill 接入统一验证 `skills/extraRoots/set` / `skills/list` 与实际 `SKILL.md` 文件身份;Chat 的安全阶段日志可通过 invocation ref 关联外部 Reference 读取。版本发布、测试 Host 切换和飞书真人门禁分别记录于[按需资料计划](../../docs/roadmap/feishu-on-demand-reference-plan.md),不以安装版本代替实际读取证据。
|
|
8
|
+
|
|
7
9
|
它负责:
|
|
8
10
|
|
|
9
11
|
- `tutti launch` 后的项目绑定、恢复和接管入口
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProviderInvocationProgress } from "../providers/invocation-progress.js";
|
|
2
|
+
export type ChatAssistantLogger = {
|
|
3
|
+
info: (fields: Record<string, unknown>, message: string) => void;
|
|
4
|
+
warn: (fields: Record<string, unknown>, message: string) => void;
|
|
5
|
+
};
|
|
6
|
+
type ChatAssistantProgress = ProviderInvocationProgress | {
|
|
7
|
+
stage: "queued" | "inspection_wait" | "model_requested" | "reply_persisted" | "failed" | "skipped";
|
|
8
|
+
reason_code?: "queue_full" | "stopping" | "context_unavailable" | "answer_failed" | "reply_failed";
|
|
9
|
+
};
|
|
10
|
+
export declare function createChatAssistantDiagnostics(options: {
|
|
11
|
+
logger?: ChatAssistantLogger;
|
|
12
|
+
projectId: string;
|
|
13
|
+
triggerMessageId: string;
|
|
14
|
+
scope: string;
|
|
15
|
+
}): (event: ChatAssistantProgress) => void;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=diagnostics.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createEventRef } from "@tutti/shared/ids";
|
|
2
|
+
export function createChatAssistantDiagnostics(options) {
|
|
3
|
+
const requestRef = createEventRef();
|
|
4
|
+
const started = performance.now();
|
|
5
|
+
return (event) => {
|
|
6
|
+
try {
|
|
7
|
+
const fields = {
|
|
8
|
+
project_id: options.projectId,
|
|
9
|
+
chat_request_ref: requestRef,
|
|
10
|
+
trigger_message_ref: options.triggerMessageId,
|
|
11
|
+
scope: options.scope,
|
|
12
|
+
elapsed_ms: Math.round(performance.now() - started),
|
|
13
|
+
...event,
|
|
14
|
+
};
|
|
15
|
+
if (event.stage === "failed" || event.stage === "provider_failed") {
|
|
16
|
+
options.logger?.warn(fields, "Chat assistant progress");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
options.logger?.info(fields, "Chat assistant progress");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* Logging must not replace the answer or interrupt queue draining. */
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ProjectId } from "@tutti/shared/ids";
|
|
2
2
|
import type { MessageProjection } from "@tutti/shared/schemas/api";
|
|
3
|
+
import { type ChatAssistantLogger } from "./diagnostics.js";
|
|
4
|
+
import type { ProviderInvocationProgressObserver } from "../providers/invocation-progress.js";
|
|
3
5
|
import { type HostProjectStore, type SqliteDatabase } from "../store/index.js";
|
|
4
6
|
import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
|
|
5
7
|
export declare const CHAT_ASSISTANT_OUTPUT_SCHEMA: {
|
|
@@ -19,6 +21,7 @@ export type ChatAssistantOutput = {
|
|
|
19
21
|
};
|
|
20
22
|
export type ChatAssistantPromptInput = {
|
|
21
23
|
prompt: string;
|
|
24
|
+
onProgress?: ProviderInvocationProgressObserver;
|
|
22
25
|
};
|
|
23
26
|
export type ReadOnlyChatAssistantModel = {
|
|
24
27
|
answer: (input: ChatAssistantPromptInput) => Promise<ChatAssistantOutput>;
|
|
@@ -35,6 +38,7 @@ export type ReadOnlyChatAssistantHandle = {
|
|
|
35
38
|
stopAccepting: () => void;
|
|
36
39
|
};
|
|
37
40
|
export type ReadOnlyChatAssistantOptions = {
|
|
41
|
+
logger?: ChatAssistantLogger;
|
|
38
42
|
project: {
|
|
39
43
|
project_id: ProjectId;
|
|
40
44
|
display_name: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createChatAssistantDiagnostics } from "./diagnostics.js";
|
|
1
2
|
import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMessageProjectionById, } from "../collaboration-state/index.js";
|
|
2
3
|
import { renderPromptTemplate } from "../prompt-templates/index.js";
|
|
3
4
|
import { withHostStoreTransaction, } from "../store/index.js";
|
|
@@ -111,13 +112,21 @@ export class ReadOnlyChatAssistant {
|
|
|
111
112
|
if (!this.accepting) {
|
|
112
113
|
return { triggered: false };
|
|
113
114
|
}
|
|
115
|
+
const report = createChatAssistantDiagnostics({
|
|
116
|
+
...(this.options.logger === undefined ? {} : { logger: this.options.logger }),
|
|
117
|
+
projectId: this.options.project.project_id,
|
|
118
|
+
triggerMessageId: input.message.id,
|
|
119
|
+
scope: input.message.scope.kind,
|
|
120
|
+
});
|
|
114
121
|
if (this.pending.length >= this.maxQueuedMentions) {
|
|
122
|
+
report({ stage: "failed", reason_code: "queue_full" });
|
|
115
123
|
this.appendAssistantMessageForTrigger(input.message, assistantFailureMessage(), {
|
|
116
124
|
scratchpad_source: "exclude",
|
|
117
125
|
});
|
|
118
126
|
return { triggered: true };
|
|
119
127
|
}
|
|
120
|
-
|
|
128
|
+
report({ stage: "queued" });
|
|
129
|
+
this.pending.push({ message: input.message, report });
|
|
121
130
|
this.pump();
|
|
122
131
|
return { triggered: true };
|
|
123
132
|
}
|
|
@@ -132,7 +141,8 @@ export class ReadOnlyChatAssistant {
|
|
|
132
141
|
return;
|
|
133
142
|
}
|
|
134
143
|
this.accepting = false;
|
|
135
|
-
for (const message of this.pending.splice(0)) {
|
|
144
|
+
for (const { message, report } of this.pending.splice(0)) {
|
|
145
|
+
report({ stage: "skipped", reason_code: "stopping" });
|
|
136
146
|
this.appendAssistantMessageForTrigger(message, assistantFailureMessage(), {
|
|
137
147
|
scratchpad_source: "exclude",
|
|
138
148
|
});
|
|
@@ -143,12 +153,14 @@ export class ReadOnlyChatAssistant {
|
|
|
143
153
|
if (this.active !== null) {
|
|
144
154
|
return;
|
|
145
155
|
}
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
156
|
+
const pending = this.pending.shift();
|
|
157
|
+
if (pending === undefined) {
|
|
148
158
|
this.resolveIdleIfNeeded();
|
|
149
159
|
return;
|
|
150
160
|
}
|
|
151
|
-
const task = this.respond(message).catch(() =>
|
|
161
|
+
const task = this.respond(pending.message, pending.report).catch(() => {
|
|
162
|
+
pending.report({ stage: "failed", reason_code: "reply_failed" });
|
|
163
|
+
});
|
|
152
164
|
this.active = task;
|
|
153
165
|
void task.finally(() => {
|
|
154
166
|
if (this.active === task) {
|
|
@@ -311,23 +323,34 @@ export class ReadOnlyChatAssistant {
|
|
|
311
323
|
}
|
|
312
324
|
this.appendMainChatAssistantMessage(triggerMessage, body, refs);
|
|
313
325
|
}
|
|
314
|
-
async respond(triggerMessage) {
|
|
326
|
+
async respond(triggerMessage, report) {
|
|
327
|
+
let answer;
|
|
315
328
|
try {
|
|
329
|
+
report({ stage: "inspection_wait" });
|
|
316
330
|
await this.options.waitForInspectionAvailability?.();
|
|
317
331
|
const prompt = this.renderPrompt(triggerMessage);
|
|
318
332
|
if (prompt === null) {
|
|
333
|
+
report({ stage: "skipped", reason_code: "context_unavailable" });
|
|
319
334
|
return;
|
|
320
335
|
}
|
|
336
|
+
report({ stage: "model_requested" });
|
|
321
337
|
const output = await this.options.model.answer({
|
|
322
338
|
prompt,
|
|
339
|
+
onProgress: report,
|
|
323
340
|
});
|
|
324
|
-
|
|
341
|
+
answer = normalizeAnswer(output.answer);
|
|
325
342
|
}
|
|
326
343
|
catch {
|
|
344
|
+
report({ stage: "failed", reason_code: "answer_failed" });
|
|
327
345
|
this.appendAssistantMessageForTrigger(triggerMessage, assistantFailureMessage(), {
|
|
328
346
|
scratchpad_source: "exclude",
|
|
329
347
|
});
|
|
348
|
+
report({ stage: "reply_persisted" });
|
|
349
|
+
return;
|
|
330
350
|
}
|
|
351
|
+
// Persistence/outbound-hook failure must not retry by creating a second reply.
|
|
352
|
+
this.appendAssistantMessageForTrigger(triggerMessage, answer);
|
|
353
|
+
report({ stage: "reply_persisted" });
|
|
331
354
|
}
|
|
332
355
|
}
|
|
333
356
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ProviderInvocationProgress = {
|
|
2
|
+
stage: "provider_queued" | "provider_started" | "context_ready" | "skills_ready" | "turn_started" | "turn_completed" | "provider_failed";
|
|
3
|
+
invocation_ref?: string;
|
|
4
|
+
skill_count?: number;
|
|
5
|
+
completed_tool_count?: number;
|
|
6
|
+
reason_code?: "provider_unavailable" | "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "file_read_not_observed" | "write_detected" | "secret_leak_detected" | "process_lifecycle_failed" | "admission_unavailable";
|
|
7
|
+
};
|
|
8
|
+
export type ProviderInvocationProgressObserver = (event: ProviderInvocationProgress) => void;
|
|
9
|
+
export declare function reportProviderInvocationProgress(observer: ProviderInvocationProgressObserver | undefined, event: ProviderInvocationProgress): void;
|
|
10
|
+
//# sourceMappingURL=invocation-progress.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProviderInvocationProgressObserver } from "../../invocation-progress.js";
|
|
1
2
|
import { type CodexAppServerLifecycleIncident } from "./invocation-lifecycle.js";
|
|
2
3
|
import { type CodexAppServerInvocationCoordinator, type CodexAppServerInvocationLease } from "./invocation-coordinator.js";
|
|
3
4
|
import { type CodexAppServerRuntimeTelemetryOptions } from "./runtime-telemetry.js";
|
|
@@ -5,6 +6,7 @@ import { type CodexAppServerAgentContextRuntime, type CodexAppServerSkillSelecti
|
|
|
5
6
|
import { type OpenAiTokenUsage } from "../token-usage.js";
|
|
6
7
|
export type CodexAppServerWebSearchMode = "disabled" | "cached" | "live";
|
|
7
8
|
export type CodexAppServerReadOnlyProcedureInput<TOutput> = {
|
|
9
|
+
onProgress?: ProviderInvocationProgressObserver;
|
|
8
10
|
apiKey: string;
|
|
9
11
|
apiBaseUrl?: string;
|
|
10
12
|
defaultModel?: string;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
2
|
import { mkdirSync, mkdtempSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { discoverCodexAppServerSkills } from "./skill-discovery.js";
|
|
6
|
+
import { reportProviderInvocationProgress, } from "../../invocation-progress.js";
|
|
5
7
|
import { buildCodexAppServerProcessPlan } from "../codex-app-server.js";
|
|
6
8
|
import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
|
|
7
9
|
import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError } from "./json-rpc.js";
|
|
@@ -113,6 +115,7 @@ function structuredOutputDetails(client, text) {
|
|
|
113
115
|
}
|
|
114
116
|
export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
115
117
|
if (options.invocationCoordinator !== undefined && options.invocationLease === undefined) {
|
|
118
|
+
reportProviderInvocationProgress(options.onProgress, { stage: "provider_queued" });
|
|
116
119
|
let lease;
|
|
117
120
|
try {
|
|
118
121
|
lease = await options.invocationCoordinator.acquire();
|
|
@@ -132,6 +135,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
132
135
|
lease.release();
|
|
133
136
|
}
|
|
134
137
|
}
|
|
138
|
+
reportProviderInvocationProgress(options.onProgress, { stage: "provider_started" });
|
|
135
139
|
let skillInputs;
|
|
136
140
|
let contextRuntimeHint;
|
|
137
141
|
try {
|
|
@@ -167,6 +171,13 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
167
171
|
options.builtInSkillsRoot ?? "",
|
|
168
172
|
]));
|
|
169
173
|
}
|
|
174
|
+
if (contextRuntimeHint !== undefined)
|
|
175
|
+
reportProviderInvocationProgress(options.onProgress, {
|
|
176
|
+
stage: "context_ready",
|
|
177
|
+
...(contextRuntimeHint.invocation_ref === undefined
|
|
178
|
+
? {}
|
|
179
|
+
: { invocation_ref: contextRuntimeHint.invocation_ref }),
|
|
180
|
+
});
|
|
170
181
|
const model = options.defaultModel ?? DEFAULT_OPENAI_MODEL;
|
|
171
182
|
const tempRoot = mkdtempSync(join(tmpdir(), "tutti-codex-readonly-procedure-"));
|
|
172
183
|
const codexHome = options.session?.codexHome ?? join(tempRoot, "codex-home");
|
|
@@ -223,7 +234,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
223
234
|
options.userSkillsRoot ?? "",
|
|
224
235
|
options.builtInSkillsRoot ?? "",
|
|
225
236
|
...(contextRuntimeHint?.sensitiveValues ?? []),
|
|
226
|
-
...skillInputs.
|
|
237
|
+
...skillInputs.flatMap((skill) => [skill.path, dirname(skill.path)]),
|
|
227
238
|
];
|
|
228
239
|
const strictLeakCheckValues = [options.apiKey, ...(contextRuntimeHint?.sensitiveValues ?? [])];
|
|
229
240
|
let primaryError;
|
|
@@ -248,6 +259,16 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
248
259
|
if (!isApiKeyAccount(loginResponse)) {
|
|
249
260
|
throw new CodexAppServerReadOnlyProcedureError("auth_failed", "Codex app-server did not accept API key auth.");
|
|
250
261
|
}
|
|
262
|
+
await discoverCodexAppServerSkills({
|
|
263
|
+
client,
|
|
264
|
+
cwd: appServerCwd,
|
|
265
|
+
skills: skillInputs,
|
|
266
|
+
requestTimeoutMs,
|
|
267
|
+
});
|
|
268
|
+
reportProviderInvocationProgress(options.onProgress, {
|
|
269
|
+
stage: "skills_ready",
|
|
270
|
+
skill_count: skillInputs.length,
|
|
271
|
+
});
|
|
251
272
|
let threadId;
|
|
252
273
|
if (options.session?.threadId === undefined) {
|
|
253
274
|
const threadStartResponse = await client.request("thread/start", {
|
|
@@ -298,6 +319,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
298
319
|
modelProvider,
|
|
299
320
|
outputSchema: options.outputSchema,
|
|
300
321
|
}, requestTimeoutMs);
|
|
322
|
+
reportProviderInvocationProgress(options.onProgress, { stage: "turn_started" });
|
|
301
323
|
const turnCompletedPromise = client.waitForNotification((notification) => notification.method === "turn/completed", turnTimeoutMs);
|
|
302
324
|
const serverRequestPromise = client.waitForServerRequest(turnTimeoutMs);
|
|
303
325
|
let completedOrRequest;
|
|
@@ -350,6 +372,10 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
350
372
|
const agentText = client.notifications
|
|
351
373
|
.map((notification) => extractAgentText(notification))
|
|
352
374
|
.join("");
|
|
375
|
+
reportProviderInvocationProgress(options.onProgress, {
|
|
376
|
+
stage: "turn_completed",
|
|
377
|
+
completed_tool_count: structuredOutputDetails(client, agentText).completed_tool_count ?? 0,
|
|
378
|
+
});
|
|
353
379
|
let parsed;
|
|
354
380
|
try {
|
|
355
381
|
parsed = parseCodexAppServerStructuredJson(agentText);
|
|
@@ -408,6 +434,9 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
408
434
|
if (error instanceof CodexAppServerReadOnlyProcedureError) {
|
|
409
435
|
primaryError = error;
|
|
410
436
|
}
|
|
437
|
+
else if (error instanceof CodexAppServerSkillSelectionError) {
|
|
438
|
+
primaryError = new CodexAppServerReadOnlyProcedureError("skill_selection_failed", error.message);
|
|
439
|
+
}
|
|
411
440
|
else if (error instanceof CodexAppServerProtocolError) {
|
|
412
441
|
primaryError = new CodexAppServerReadOnlyProcedureError("app_server_protocol_error", redactWithKnownValues(error.message, sensitiveValues));
|
|
413
442
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CodexAppServerSkillInput } from "./skills.js";
|
|
2
|
+
export declare function discoverCodexAppServerSkills(options: {
|
|
3
|
+
client: {
|
|
4
|
+
request: (method: string, params: unknown, timeoutMs: number) => Promise<unknown>;
|
|
5
|
+
};
|
|
6
|
+
cwd: string;
|
|
7
|
+
skills: CodexAppServerSkillInput[];
|
|
8
|
+
requestTimeoutMs: number;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=skill-discovery.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { isRecord } from "./runtime-helpers.js";
|
|
4
|
+
import { CodexAppServerSkillSelectionError } from "./skills.js";
|
|
5
|
+
// Use the pinned binary's protocol, not fields accepted only by a newer client.
|
|
6
|
+
// Extra roots are process-local; each invocation owns its app-server process.
|
|
7
|
+
export async function discoverCodexAppServerSkills(options) {
|
|
8
|
+
if (options.skills.length === 0)
|
|
9
|
+
return;
|
|
10
|
+
try {
|
|
11
|
+
const cwd = realpathSync(options.cwd);
|
|
12
|
+
await options.client.request("skills/extraRoots/set", { extraRoots: [...new Set(options.skills.map((skill) => dirname(skill.path)))] }, options.requestTimeoutMs);
|
|
13
|
+
const response = await options.client.request("skills/list", { cwds: [cwd], forceReload: true }, options.requestTimeoutMs);
|
|
14
|
+
const entry = isRecord(response) && Array.isArray(response.data)
|
|
15
|
+
? response.data.find((entry) => isRecord(entry) && entry.cwd === cwd)
|
|
16
|
+
: undefined;
|
|
17
|
+
const discovered = isRecord(entry) && Array.isArray(entry.skills) ? entry.skills : [];
|
|
18
|
+
if (!options.skills.every((skill) => discovered.some((candidate) => isRecord(candidate) &&
|
|
19
|
+
candidate.name === skill.name &&
|
|
20
|
+
candidate.path === skill.path &&
|
|
21
|
+
candidate.enabled === true)))
|
|
22
|
+
throw new Error("Selected skill was not discovered");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// No discovered paths, parser errors or arbitrary RPC error bodies in diagnostics.
|
|
26
|
+
throw new CodexAppServerSkillSelectionError("skill_discovery_failed", "Codex app-server could not discover the selected skills.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=skill-discovery.js.map
|
|
@@ -30,27 +30,21 @@ export type CodexAppServerAgentContextTokenRequest = {
|
|
|
30
30
|
export type CodexAppServerAgentContextToken = {
|
|
31
31
|
token: string;
|
|
32
32
|
expires_at?: string;
|
|
33
|
+
invocation_ref?: string;
|
|
33
34
|
};
|
|
34
35
|
export type CodexAppServerAgentContextRuntime = {
|
|
35
36
|
baseUrl: string;
|
|
36
37
|
issueToken: (input: CodexAppServerAgentContextTokenRequest) => CodexAppServerAgentContextToken;
|
|
37
38
|
};
|
|
38
|
-
export type CodexAppServerSkillsListWarmup = {
|
|
39
|
-
cwds: string[];
|
|
40
|
-
forceReload: true;
|
|
41
|
-
perCwdExtraUserRoots: Array<{
|
|
42
|
-
cwd: string;
|
|
43
|
-
extraUserRoots: string[];
|
|
44
|
-
}>;
|
|
45
|
-
};
|
|
46
39
|
export type CodexAppServerContextRuntimeHint = {
|
|
47
40
|
text: string;
|
|
48
41
|
sensitiveValues: string[];
|
|
49
42
|
scopes: CodexAppServerContextApiScope[];
|
|
43
|
+
invocation_ref?: string;
|
|
50
44
|
};
|
|
51
45
|
export declare class CodexAppServerSkillSelectionError extends Error {
|
|
52
|
-
readonly code: "built_in_skill_not_found" | "built_in_skill_invalid";
|
|
53
|
-
constructor(code: "built_in_skill_not_found" | "built_in_skill_invalid", message: string);
|
|
46
|
+
readonly code: "built_in_skill_not_found" | "built_in_skill_invalid" | "skill_discovery_failed";
|
|
47
|
+
constructor(code: "built_in_skill_not_found" | "built_in_skill_invalid" | "skill_discovery_failed", message: string);
|
|
54
48
|
}
|
|
55
49
|
export declare const CODEX_APP_SERVER_SKILL_SELECTIONS: Record<CodexAppServerSkillSelectionName, CodexAppServerSkillSelection>;
|
|
56
50
|
export declare function resolveCodexAppServerSkillSelection(selectionName: CodexAppServerSkillSelectionName): CodexAppServerSkillSelection;
|
|
@@ -60,12 +54,6 @@ export declare function resolveCodexAppServerSkillInputs(options: {
|
|
|
60
54
|
builtInSkillsRoot?: string;
|
|
61
55
|
userSkillsRoot?: string;
|
|
62
56
|
}): CodexAppServerSkillInput[];
|
|
63
|
-
export declare function resolveCodexAppServerSkillsListWarmup(options: {
|
|
64
|
-
cwd: string;
|
|
65
|
-
selectionName?: CodexAppServerSkillSelectionName;
|
|
66
|
-
selection?: CodexAppServerSkillSelection;
|
|
67
|
-
userSkillsRoot?: string;
|
|
68
|
-
}): CodexAppServerSkillsListWarmup | undefined;
|
|
69
57
|
export declare function buildCodexAppServerTurnInput(options: {
|
|
70
58
|
prompt: string;
|
|
71
59
|
skills?: CodexAppServerSkillInput[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { listUserSkills } from "../../../skills/index.js";
|
|
@@ -121,12 +121,12 @@ function resolveBuiltInSkill(options) {
|
|
|
121
121
|
if (!statSync(skillMdPath).isFile()) {
|
|
122
122
|
throw new CodexAppServerSkillSelectionError("built_in_skill_invalid", `Built-in skill is invalid: ${options.name}`);
|
|
123
123
|
}
|
|
124
|
-
return { name: options.name, path };
|
|
124
|
+
return { name: options.name, path: realpathSync(skillMdPath) };
|
|
125
125
|
}
|
|
126
126
|
function resolveUserSkill(options) {
|
|
127
127
|
return {
|
|
128
128
|
name: options.name,
|
|
129
|
-
path: join(options.userSkillsRoot, options.name),
|
|
129
|
+
path: realpathSync(join(options.userSkillsRoot, options.name, "SKILL.md")),
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
export function resolveCodexAppServerSkillSelection(selectionName) {
|
|
@@ -149,32 +149,6 @@ export function resolveCodexAppServerSkillInputs(options) {
|
|
|
149
149
|
: [];
|
|
150
150
|
return [...builtInSkills, ...userSkills];
|
|
151
151
|
}
|
|
152
|
-
export function resolveCodexAppServerSkillsListWarmup(options) {
|
|
153
|
-
const selection = options.selection ??
|
|
154
|
-
(options.selectionName === undefined
|
|
155
|
-
? {
|
|
156
|
-
builtInSkillNames: [],
|
|
157
|
-
userSkillMode: "disabled",
|
|
158
|
-
contextApiScopes: [],
|
|
159
|
-
}
|
|
160
|
-
: resolveCodexAppServerSkillSelection(options.selectionName));
|
|
161
|
-
if (selection.userSkillMode !== "all_enabled" || options.userSkillsRoot === undefined) {
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
|
-
if (listUserSkills(options.userSkillsRoot).length === 0) {
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
cwds: [options.cwd],
|
|
169
|
-
forceReload: true,
|
|
170
|
-
perCwdExtraUserRoots: [
|
|
171
|
-
{
|
|
172
|
-
cwd: options.cwd,
|
|
173
|
-
extraUserRoots: [options.userSkillsRoot],
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
152
|
function createTextInputItem(text) {
|
|
179
153
|
return {
|
|
180
154
|
type: "text",
|
|
@@ -228,6 +202,7 @@ export function resolveCodexAppServerContextRuntimeHint(options) {
|
|
|
228
202
|
}),
|
|
229
203
|
sensitiveValues: [issued.token, options.agentContext.baseUrl],
|
|
230
204
|
scopes: selection.contextApiScopes,
|
|
205
|
+
...(issued.invocation_ref === undefined ? {} : { invocation_ref: issued.invocation_ref }),
|
|
231
206
|
};
|
|
232
207
|
}
|
|
233
208
|
//# sourceMappingURL=skills.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdirSync, mkdtempSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { discoverCodexAppServerSkills } from "./skill-discovery.js";
|
|
5
6
|
import { buildCodexAppServerProcessPlan } from "../codex-app-server.js";
|
|
6
7
|
import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
|
|
7
8
|
import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError, } from "./json-rpc.js";
|
|
@@ -10,7 +11,7 @@ import { CodexAppServerAdmissionError, } from "./invocation-coordinator.js";
|
|
|
10
11
|
import { createCodexAppServerRuntimeObserver, } from "./runtime-telemetry.js";
|
|
11
12
|
import { createReadOnlySandboxPolicy, createWorkspaceWriteSandboxPolicy, } from "./sandbox-policy.js";
|
|
12
13
|
import { buildCodexAppServerProviderConfigOverrides, resolveCodexAppServerModelProvider, } from "./provider-config.js";
|
|
13
|
-
import { buildCodexAppServerTurnInput, resolveCodexAppServerContextRuntimeHint,
|
|
14
|
+
import { buildCodexAppServerTurnInput, resolveCodexAppServerContextRuntimeHint, resolveCodexAppServerSkillInputs, CodexAppServerSkillSelectionError, } from "./skills.js";
|
|
14
15
|
import { compactText, containsKnownSecret, createCodexSpawnEnv, extractAgentText, extractServerRequestMethod, findJsonObjectCandidates, isApiKeyAccount, readThreadId, readTurnError, readTurnStatus, redactWithKnownValues, } from "./runtime-helpers.js";
|
|
15
16
|
import { extractCodexAppServerTokenUsage } from "../token-usage.js";
|
|
16
17
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
@@ -170,7 +171,6 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
let skillInputs;
|
|
173
|
-
let skillsListWarmup;
|
|
174
174
|
let contextRuntimeHint;
|
|
175
175
|
try {
|
|
176
176
|
skillInputs = resolveCodexAppServerSkillInputs({
|
|
@@ -182,13 +182,6 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
|
|
|
182
182
|
? {}
|
|
183
183
|
: { builtInSkillsRoot: options.builtInSkillsRoot }),
|
|
184
184
|
});
|
|
185
|
-
skillsListWarmup = resolveCodexAppServerSkillsListWarmup({
|
|
186
|
-
cwd: options.workspaceRoot,
|
|
187
|
-
...(options.skillSelectionName === undefined
|
|
188
|
-
? {}
|
|
189
|
-
: { selectionName: options.skillSelectionName }),
|
|
190
|
-
...(options.userSkillsRoot === undefined ? {} : { userSkillsRoot: options.userSkillsRoot }),
|
|
191
|
-
});
|
|
192
185
|
contextRuntimeHint = resolveCodexAppServerContextRuntimeHint({
|
|
193
186
|
...(options.approvalContext?.activityRef === undefined
|
|
194
187
|
? {}
|
|
@@ -267,11 +260,7 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
|
|
|
267
260
|
options.userSkillsRoot ?? "",
|
|
268
261
|
options.builtInSkillsRoot ?? "",
|
|
269
262
|
...(contextRuntimeHint?.sensitiveValues ?? []),
|
|
270
|
-
...skillInputs.
|
|
271
|
-
...(skillsListWarmup?.perCwdExtraUserRoots.flatMap((entry) => [
|
|
272
|
-
entry.cwd,
|
|
273
|
-
...entry.extraUserRoots,
|
|
274
|
-
]) ?? []),
|
|
263
|
+
...skillInputs.flatMap((skill) => [skill.path, dirname(skill.path)]),
|
|
275
264
|
];
|
|
276
265
|
const strictLeakCheckValues = [options.apiKey, ...(contextRuntimeHint?.sensitiveValues ?? [])];
|
|
277
266
|
let primaryError;
|
|
@@ -297,9 +286,12 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
|
|
|
297
286
|
if (!isApiKeyAccount(loginResponse)) {
|
|
298
287
|
throw new CodexAppServerWorkspaceWriteRunError("auth_failed", "Codex app-server did not accept API key auth.");
|
|
299
288
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
289
|
+
await discoverCodexAppServerSkills({
|
|
290
|
+
client,
|
|
291
|
+
cwd: options.workspaceRoot,
|
|
292
|
+
skills: skillInputs,
|
|
293
|
+
requestTimeoutMs,
|
|
294
|
+
});
|
|
303
295
|
let threadId;
|
|
304
296
|
if (options.session?.threadId === undefined) {
|
|
305
297
|
const threadStartResponse = await client.request("thread/start", {
|
|
@@ -484,7 +476,9 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
|
|
|
484
476
|
}
|
|
485
477
|
else {
|
|
486
478
|
const message = error instanceof Error ? error.message : String(error);
|
|
487
|
-
primaryError = new CodexAppServerWorkspaceWriteRunError(
|
|
479
|
+
primaryError = new CodexAppServerWorkspaceWriteRunError(error instanceof CodexAppServerSkillSelectionError
|
|
480
|
+
? "skill_selection_failed"
|
|
481
|
+
: "app_server_protocol_error", redactWithKnownValues(message, sensitiveValues), { turn_start_state: turnStartState });
|
|
488
482
|
}
|
|
489
483
|
}
|
|
490
484
|
finally {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { CHAT_ASSISTANT_OUTPUT_SCHEMA, isChatAssistantOutput, } from "../../chat-assistant/index.js";
|
|
2
2
|
import { readOpenAiApiKeyCredential } from "./credential-store.js";
|
|
3
|
-
import { resolveOpenAiProviderConfig
|
|
4
|
-
import { runCodexAppServerReadOnlyProcedure } from "./app-server/read-only-procedure.js";
|
|
3
|
+
import { resolveOpenAiProviderConfig } from "./provider-config.js";
|
|
4
|
+
import { CodexAppServerReadOnlyProcedureError, runCodexAppServerReadOnlyProcedure, } from "./app-server/read-only-procedure.js";
|
|
5
|
+
import { reportProviderInvocationProgress } from "../invocation-progress.js";
|
|
5
6
|
import { getProjectUserSkillsRoot } from "../../skills/index.js";
|
|
6
7
|
export function createOpenAiReadOnlyChatAssistantModel(options) {
|
|
7
8
|
return {
|
|
8
9
|
async answer(input) {
|
|
9
10
|
const result = await runCodexAppServerReadOnlyProcedure({
|
|
11
|
+
...(input.onProgress === undefined ? {} : { onProgress: input.onProgress }),
|
|
10
12
|
apiKey: options.apiKey,
|
|
11
13
|
workspaceRoot: options.workspaceRoot,
|
|
12
14
|
prompt: input.prompt,
|
|
@@ -37,6 +39,14 @@ export function createOpenAiReadOnlyChatAssistantModel(options) {
|
|
|
37
39
|
...(options.onLifecycleIncident === undefined
|
|
38
40
|
? {}
|
|
39
41
|
: { onLifecycleIncident: options.onLifecycleIncident }),
|
|
42
|
+
}).catch((error) => {
|
|
43
|
+
reportProviderInvocationProgress(input.onProgress, {
|
|
44
|
+
stage: "provider_failed",
|
|
45
|
+
reason_code: error instanceof CodexAppServerReadOnlyProcedureError
|
|
46
|
+
? error.code
|
|
47
|
+
: "provider_unavailable",
|
|
48
|
+
});
|
|
49
|
+
throw error;
|
|
40
50
|
});
|
|
41
51
|
if (options.recordUsage !== undefined && result.usage !== undefined) {
|
|
42
52
|
options.recordUsage({
|
|
@@ -137,6 +137,7 @@ function createAgentContextRuntime(options) {
|
|
|
137
137
|
return {
|
|
138
138
|
token: token.token,
|
|
139
139
|
expires_at: token.expires_at,
|
|
140
|
+
invocation_ref: token.reference_invocation_ref,
|
|
140
141
|
};
|
|
141
142
|
},
|
|
142
143
|
};
|
|
@@ -419,6 +420,7 @@ export async function startForegroundHostServer(options) {
|
|
|
419
420
|
}),
|
|
420
421
|
});
|
|
421
422
|
const chatAssistant = new ReadOnlyChatAssistant({
|
|
423
|
+
logger: createHostRuntimeLogger({ component: "chat-assistant", logFilePath, now }),
|
|
422
424
|
project: chatAssistantProject,
|
|
423
425
|
store,
|
|
424
426
|
events: workspaceEvents,
|
|
@@ -16,20 +16,43 @@ export function registerAgentContextExternalReferenceRoutes(app, options) {
|
|
|
16
16
|
}, async (request, reply) => {
|
|
17
17
|
reply.header("cache-control", "no-store");
|
|
18
18
|
const invocation = authorizeAgentContextRequest(request, options, "references:external");
|
|
19
|
+
const started = performance.now();
|
|
20
|
+
const diagnosticFields = {
|
|
21
|
+
action,
|
|
22
|
+
invocation_ref: invocation.reference_invocation_ref,
|
|
23
|
+
activity_ref: invocation.activity_ref,
|
|
24
|
+
};
|
|
25
|
+
request.log.info({ ...diagnosticFields, stage: "external_reference_request" }, "External Reference request started");
|
|
19
26
|
const service = options.externalReferences;
|
|
20
|
-
if (!service)
|
|
27
|
+
if (!service) {
|
|
28
|
+
request.log.info({
|
|
29
|
+
...diagnosticFields,
|
|
30
|
+
stage: "external_reference_unavailable",
|
|
31
|
+
reason_code: "external_references_not_enabled",
|
|
32
|
+
}, "External Reference service unavailable");
|
|
21
33
|
return { kind: "unavailable", reason: "external_references_not_enabled" };
|
|
34
|
+
}
|
|
22
35
|
const sourceRef = request.query.source_ref;
|
|
23
36
|
if (action === "read" &&
|
|
24
37
|
(sourceRef === undefined || !/^external_ref_[a-f0-9]{32}$/u.test(sourceRef)))
|
|
25
38
|
return { kind: "unavailable", reason: "reference_source_invalid" };
|
|
26
39
|
try {
|
|
27
|
-
if (action === "list")
|
|
28
|
-
|
|
40
|
+
if (action === "list") {
|
|
41
|
+
const items = await service.list(invocation);
|
|
42
|
+
request.log.info({
|
|
43
|
+
...diagnosticFields,
|
|
44
|
+
stage: "external_reference_list",
|
|
45
|
+
source_count: items.length,
|
|
46
|
+
elapsed_ms: Math.round(performance.now() - started),
|
|
47
|
+
}, "External Reference sources listed");
|
|
48
|
+
return { items };
|
|
49
|
+
}
|
|
29
50
|
const result = await service.read(invocation, sourceRef);
|
|
30
51
|
const { copy } = result;
|
|
31
52
|
request.log.info({
|
|
32
53
|
stage: "external_reference_read",
|
|
54
|
+
action,
|
|
55
|
+
elapsed_ms: Math.round(performance.now() - started),
|
|
33
56
|
invocation_ref: invocation.reference_invocation_ref,
|
|
34
57
|
activity_ref: invocation.activity_ref,
|
|
35
58
|
source_ref: copy.source_ref,
|
|
@@ -70,7 +93,9 @@ export function registerAgentContextExternalReferenceRoutes(app, options) {
|
|
|
70
93
|
? "reference_cache_unsafe"
|
|
71
94
|
: "reference_read_unavailable";
|
|
72
95
|
request.log.warn({
|
|
73
|
-
stage: "
|
|
96
|
+
stage: "external_reference_unavailable",
|
|
97
|
+
action,
|
|
98
|
+
elapsed_ms: Math.round(performance.now() - started),
|
|
74
99
|
invocation_ref: invocation.reference_invocation_ref,
|
|
75
100
|
activity_ref: invocation.activity_ref,
|
|
76
101
|
...(sourceRef === undefined ? {} : { source_ref: sourceRef }),
|
|
@@ -3,18 +3,19 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import fastifyStatic from "@fastify/static";
|
|
5
5
|
const BLOCKED_SPA_FALLBACK_PREFIXES = [
|
|
6
|
-
"/api
|
|
6
|
+
"/api",
|
|
7
7
|
"/health",
|
|
8
|
-
"/host-local
|
|
9
|
-
"/local-console
|
|
10
|
-
"/assets
|
|
8
|
+
"/host-local",
|
|
9
|
+
"/local-console",
|
|
10
|
+
"/assets",
|
|
11
|
+
"/internal",
|
|
11
12
|
];
|
|
12
13
|
function requestPath(request) {
|
|
13
14
|
const queryIndex = request.url.indexOf("?");
|
|
14
15
|
return queryIndex === -1 ? request.url : request.url.slice(0, queryIndex);
|
|
15
16
|
}
|
|
16
17
|
function shouldServeSpaFallback(path) {
|
|
17
|
-
return !BLOCKED_SPA_FALLBACK_PREFIXES.some((prefix) => path === prefix || path.startsWith(prefix));
|
|
18
|
+
return !BLOCKED_SPA_FALLBACK_PREFIXES.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
|
|
18
19
|
}
|
|
19
20
|
function sendIndex(reply, indexHtml) {
|
|
20
21
|
void reply
|
|
@@ -39,7 +40,10 @@ export function registerHostWebStaticRoutes(app, options) {
|
|
|
39
40
|
});
|
|
40
41
|
app.get("/*", (request, reply) => {
|
|
41
42
|
if (!shouldServeSpaFallback(requestPath(request))) {
|
|
42
|
-
void reply
|
|
43
|
+
void reply
|
|
44
|
+
.header("cache-control", "no-store")
|
|
45
|
+
.status(404)
|
|
46
|
+
.send({
|
|
43
47
|
error: {
|
|
44
48
|
code: "not_found",
|
|
45
49
|
message: "Route was not found",
|