subharness 0.0.3 → 0.0.5
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 +39 -9
- package/dist/adapters/approval-context.d.ts +2 -0
- package/dist/adapters/approval-context.js +9 -0
- package/dist/adapters/approval-context.js.map +1 -0
- package/dist/adapters/approval-lifetime.d.ts +8 -0
- package/dist/adapters/approval-lifetime.js +50 -0
- package/dist/adapters/approval-lifetime.js.map +1 -0
- package/dist/adapters/claude-approvals.d.ts +4 -0
- package/dist/adapters/claude-approvals.js +32 -0
- package/dist/adapters/claude-approvals.js.map +1 -0
- package/dist/adapters/claude-permissions.js +1 -1
- package/dist/adapters/claude-permissions.js.map +1 -1
- package/dist/adapters/claude-process.d.ts +3 -0
- package/dist/adapters/claude-process.js +11 -1
- package/dist/adapters/claude-process.js.map +1 -1
- package/dist/adapters/claude-result.d.ts +2 -0
- package/dist/adapters/claude-result.js +22 -0
- package/dist/adapters/claude-result.js.map +1 -0
- package/dist/adapters/claude.js +60 -12
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/codex-approvals.d.ts +6 -0
- package/dist/adapters/codex-approvals.js +111 -0
- package/dist/adapters/codex-approvals.js.map +1 -0
- package/dist/adapters/codex.js +36 -3
- package/dist/adapters/codex.js.map +1 -1
- package/dist/adapters/copilot-permissions.d.ts +26 -0
- package/dist/adapters/copilot-permissions.js +121 -0
- package/dist/adapters/copilot-permissions.js.map +1 -0
- package/dist/adapters/copilot-tools.d.ts +12 -0
- package/dist/adapters/copilot-tools.js +61 -0
- package/dist/adapters/copilot-tools.js.map +1 -0
- package/dist/adapters/copilot.d.ts +108 -0
- package/dist/adapters/copilot.js +819 -0
- package/dist/adapters/copilot.js.map +1 -0
- package/dist/adapters/cursor-cli-approvals.d.ts +2 -0
- package/dist/adapters/cursor-cli-approvals.js +83 -0
- package/dist/adapters/cursor-cli-approvals.js.map +1 -0
- package/dist/adapters/cursor-cli-model.d.ts +4 -0
- package/dist/adapters/cursor-cli-model.js +64 -0
- package/dist/adapters/cursor-cli-model.js.map +1 -0
- package/dist/adapters/cursor-cli-session.d.ts +32 -0
- package/dist/adapters/cursor-cli-session.js +316 -0
- package/dist/adapters/cursor-cli-session.js.map +1 -0
- package/dist/adapters/cursor-cli-tools.d.ts +20 -0
- package/dist/adapters/cursor-cli-tools.js +181 -0
- package/dist/adapters/cursor-cli-tools.js.map +1 -0
- package/dist/adapters/cursor-cli.d.ts +2 -0
- package/dist/adapters/cursor-cli.js +306 -0
- package/dist/adapters/cursor-cli.js.map +1 -0
- package/dist/adapters/cursor-model.d.ts +7 -0
- package/dist/adapters/cursor-model.js +67 -0
- package/dist/adapters/cursor-model.js.map +1 -0
- package/dist/adapters/cursor-rpc.d.ts +42 -0
- package/dist/adapters/cursor-rpc.js +271 -0
- package/dist/adapters/cursor-rpc.js.map +1 -0
- package/dist/adapters/cursor-session.d.ts +4 -0
- package/dist/adapters/cursor-session.js +327 -0
- package/dist/adapters/cursor-session.js.map +1 -0
- package/dist/adapters/cursor-startup.d.ts +16 -0
- package/dist/adapters/cursor-startup.js +74 -0
- package/dist/adapters/cursor-startup.js.map +1 -0
- package/dist/adapters/cursor-tools.d.ts +11 -0
- package/dist/adapters/cursor-tools.js +49 -0
- package/dist/adapters/cursor-tools.js.map +1 -0
- package/dist/adapters/cursor.d.ts +6 -0
- package/dist/adapters/cursor.js +17 -0
- package/dist/adapters/cursor.js.map +1 -0
- package/dist/adapters/fx-approvals.d.ts +2 -0
- package/dist/adapters/fx-approvals.js +14 -0
- package/dist/adapters/fx-approvals.js.map +1 -0
- package/dist/adapters/fx-auth.d.ts +12 -2
- package/dist/adapters/fx-auth.js +51 -61
- package/dist/adapters/fx-auth.js.map +1 -1
- package/dist/adapters/fx-profile.d.ts +8 -0
- package/dist/adapters/fx-profile.js +101 -0
- package/dist/adapters/fx-profile.js.map +1 -0
- package/dist/adapters/fx-rpc.d.ts +2 -0
- package/dist/adapters/fx-rpc.js +30 -4
- package/dist/adapters/fx-rpc.js.map +1 -1
- package/dist/adapters/fx-status.d.ts +2 -0
- package/dist/adapters/fx-status.js +96 -0
- package/dist/adapters/fx-status.js.map +1 -0
- package/dist/adapters/fx.js +77 -30
- package/dist/adapters/fx.js.map +1 -1
- package/dist/adapters/opencode-access.d.ts +13 -0
- package/dist/adapters/opencode-access.js +76 -0
- package/dist/adapters/opencode-access.js.map +1 -0
- package/dist/adapters/opencode-config.d.ts +11 -0
- package/dist/adapters/opencode-config.js +238 -0
- package/dist/adapters/opencode-config.js.map +1 -0
- package/dist/adapters/opencode-http.d.ts +28 -0
- package/dist/adapters/opencode-http.js +297 -0
- package/dist/adapters/opencode-http.js.map +1 -0
- package/dist/adapters/opencode-tools.d.ts +19 -0
- package/dist/adapters/opencode-tools.js +127 -0
- package/dist/adapters/opencode-tools.js.map +1 -0
- package/dist/adapters/opencode.d.ts +2 -0
- package/dist/adapters/opencode.js +569 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/adapters/rpc.d.ts +3 -0
- package/dist/adapters/rpc.js +45 -4
- package/dist/adapters/rpc.js.map +1 -1
- package/dist/adapters/types.d.ts +7 -0
- package/dist/adapters/types.js.map +1 -1
- package/dist/approvals/schema.d.ts +4 -0
- package/dist/approvals/schema.js +70 -0
- package/dist/approvals/schema.js.map +1 -0
- package/dist/approvals/types.d.ts +47 -0
- package/dist/approvals/types.js +2 -0
- package/dist/approvals/types.js.map +1 -0
- package/dist/cli/args.d.ts +3 -2
- package/dist/cli/args.js +24 -3
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/dashboard-client.d.ts +5 -1
- package/dist/cli/dashboard-client.js +162 -15
- package/dist/cli/dashboard-client.js.map +1 -1
- package/dist/cli/dashboard-controller.d.ts +7 -0
- package/dist/cli/dashboard-controller.js +27 -0
- package/dist/cli/dashboard-controller.js.map +1 -0
- package/dist/cli/dashboard-detail-view.d.ts +16 -0
- package/dist/cli/dashboard-detail-view.js +162 -0
- package/dist/cli/dashboard-detail-view.js.map +1 -0
- package/dist/cli/dashboard-history-view.d.ts +29 -0
- package/dist/cli/dashboard-history-view.js +211 -0
- package/dist/cli/dashboard-history-view.js.map +1 -0
- package/dist/cli/dashboard-input.d.ts +14 -0
- package/dist/cli/dashboard-input.js +128 -0
- package/dist/cli/dashboard-input.js.map +1 -0
- package/dist/cli/dashboard-layout.d.ts +12 -1
- package/dist/cli/dashboard-layout.js +32 -30
- package/dist/cli/dashboard-layout.js.map +1 -1
- package/dist/cli/dashboard-renderer.d.ts +30 -2
- package/dist/cli/dashboard-renderer.js +125 -7
- package/dist/cli/dashboard-renderer.js.map +1 -1
- package/dist/cli/dashboard-style.d.ts +9 -0
- package/dist/cli/dashboard-style.js +36 -0
- package/dist/cli/dashboard-style.js.map +1 -0
- package/dist/cli/dashboard.d.ts +5 -3
- package/dist/cli/dashboard.js +102 -12
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +37 -12
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/input.d.ts +1 -0
- package/dist/cli/input.js +23 -17
- package/dist/cli/input.js.map +1 -1
- package/dist/cli/main.js +6 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/output.js +17 -1
- package/dist/cli/output.js.map +1 -1
- package/dist/config/access-provenance.d.ts +10 -0
- package/dist/config/access-provenance.js +31 -0
- package/dist/config/access-provenance.js.map +1 -0
- package/dist/config/access.d.ts +12 -2
- package/dist/config/access.js +125 -27
- package/dist/config/access.js.map +1 -1
- package/dist/config/loader.js +1 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/oidc.js +4 -2
- package/dist/config/oidc.js.map +1 -1
- package/dist/config/project.js +2 -1
- package/dist/config/project.js.map +1 -1
- package/dist/config/resolve-access.js +24 -10
- package/dist/config/resolve-access.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/process-diagnostics.d.ts +2 -0
- package/dist/process-diagnostics.js +11 -0
- package/dist/process-diagnostics.js.map +1 -0
- package/dist/runtime/access-errors.d.ts +4 -0
- package/dist/runtime/access-errors.js +25 -0
- package/dist/runtime/access-errors.js.map +1 -0
- package/dist/runtime/approval-registry.d.ts +27 -0
- package/dist/runtime/approval-registry.js +126 -0
- package/dist/runtime/approval-registry.js.map +1 -0
- package/dist/runtime/client.js +54 -14
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/coordinator.d.ts +17 -1
- package/dist/runtime/coordinator.js +124 -6
- package/dist/runtime/coordinator.js.map +1 -1
- package/dist/runtime/dashboard-sanitize.d.ts +2 -0
- package/dist/runtime/dashboard-sanitize.js +7 -0
- package/dist/runtime/dashboard-sanitize.js.map +1 -0
- package/dist/runtime/dashboard.d.ts +43 -1
- package/dist/runtime/dashboard.js +2 -3
- package/dist/runtime/dashboard.js.map +1 -1
- package/dist/runtime/definition.js +11 -1
- package/dist/runtime/definition.js.map +1 -1
- package/dist/runtime/select-native.d.ts +2 -1
- package/dist/runtime/select-native.js +28 -10
- package/dist/runtime/select-native.js.map +1 -1
- package/dist/runtime/service.js +24 -1
- package/dist/runtime/service.js.map +1 -1
- package/dist/runtime/types.d.ts +9 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/runtime/worker-approvals.d.ts +21 -0
- package/dist/runtime/worker-approvals.js +51 -0
- package/dist/runtime/worker-approvals.js.map +1 -0
- package/dist/runtime/worker-client.js +73 -20
- package/dist/runtime/worker-client.js.map +1 -1
- package/dist/runtime/worker-server.d.ts +2 -0
- package/dist/runtime/worker-server.js +39 -10
- package/dist/runtime/worker-server.js.map +1 -1
- package/dist/sdk/definitions.d.ts +4 -1
- package/dist/sdk/definitions.js +16 -2
- package/dist/sdk/definitions.js.map +1 -1
- package/dist/sdk/permission-validation.js +10 -1
- package/dist/sdk/permission-validation.js.map +1 -1
- package/dist/sdk/types.d.ts +29 -1
- package/dist/sdk/types.js.map +1 -1
- package/package.json +4 -1
- package/sdk/access-config.md +86 -4
- package/sdk/adapter-contract.md +7 -13
- package/sdk/additional-harnesses.md +43 -0
- package/sdk/agent-skill.md +7 -4
- package/sdk/agent.md +5 -3
- package/sdk/approvals.md +120 -0
- package/sdk/authentication.md +1 -1
- package/sdk/cli/dashboard-design.md +44 -4
- package/sdk/cli/dashboard-inspection.md +25 -0
- package/sdk/cli/index.md +20 -11
- package/sdk/cli/output.md +10 -4
- package/sdk/completion-notifications.md +2 -0
- package/sdk/config.md +1 -1
- package/sdk/copilot.md +53 -0
- package/sdk/cursor.md +68 -0
- package/sdk/diagnostics.md +29 -0
- package/sdk/distribution.md +53 -1
- package/sdk/evals.md +10 -4
- package/sdk/fx.md +51 -6
- package/sdk/harnesses.md +13 -13
- package/sdk/index.md +4 -2
- package/sdk/message-delivery.md +1 -1
- package/sdk/opencode.md +57 -0
- package/sdk/permissions.md +7 -5
- package/sdk/pr-integration.md +1 -1
- package/sdk/project-team.md +2 -0
- package/sdk/sessions.md +4 -1
- package/sdk/tools.md +1 -1
- package/sdk/v1-runtime.md +6 -6
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { AgentError } from "../errors.js";
|
|
7
|
+
const profileLimit = 1024 * 1024;
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
export function fxCustomConnection(settings, provider) {
|
|
12
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(provider) || ["gateway", "codex", "grok"].includes(provider.toLowerCase())) {
|
|
13
|
+
throw new AgentError("INVALID_CONFIG", "fx direct API keys require a valid native custom connection name.");
|
|
14
|
+
}
|
|
15
|
+
const definitions = settings.providers;
|
|
16
|
+
const selected = record(definitions) && Object.hasOwn(definitions, provider) ? definitions[provider] : undefined;
|
|
17
|
+
if (!record(selected))
|
|
18
|
+
throw new AgentError("INVALID_CONFIG", "The selected fx custom connection is not configured in the native profile.");
|
|
19
|
+
if (selected.protocol !== "openai-chat-completions" || !record(selected.auth) || selected.auth.type !== "bearer") {
|
|
20
|
+
throw new AgentError("UNSUPPORTED_OPTION", "fx direct API keys require a native Chat Completions connection with bearer environment authentication.");
|
|
21
|
+
}
|
|
22
|
+
const slot = selected.auth.env;
|
|
23
|
+
if (typeof slot !== "string" || !/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(slot) || unsafeCredentialSlot(slot)) {
|
|
24
|
+
throw new AgentError("INVALID_CONFIG", "The fx custom connection credential slot cannot control the process, routing, or diagnostics.");
|
|
25
|
+
}
|
|
26
|
+
const raw = selected.base_url;
|
|
27
|
+
try {
|
|
28
|
+
if (typeof raw !== "string" || raw.length > 2048 || /[^\x21-\x7e]|[\\?#]/u.test(raw) || !/^https?:\/\//iu.test(raw)
|
|
29
|
+
|| /^https?:\/\/[^/]*@/iu.test(raw))
|
|
30
|
+
throw new Error();
|
|
31
|
+
const url = new URL(raw);
|
|
32
|
+
if (!url.hostname || url.username || url.password || url.port === "0"
|
|
33
|
+
|| (url.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
34
|
+
throw new Error();
|
|
35
|
+
return { endpoint: raw.endsWith("/") ? raw.slice(0, -1) : raw, keySlot: slot };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new AgentError("INVALID_CONFIG", "The fx custom connection requires a valid HTTPS or loopback HTTP endpoint without credentials, query, or fragment.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function unsafeCredentialSlot(slot) {
|
|
42
|
+
const upper = slot.toUpperCase();
|
|
43
|
+
return ["HOME", "PATH", "SHELL", "ENV", "BASH_ENV", "ZDOTDIR", "PWD", "OLDPWD", "USER", "LOGNAME", "TMPDIR", "TMP", "TEMP",
|
|
44
|
+
"AI_GATEWAY_API_KEY", "VERCEL_OIDC_TOKEN", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY"].includes(upper)
|
|
45
|
+
|| /^(?:FX_|NODE_|LD_|DYLD_|XDG_|SSL_|OPENSSL_|SUBHARNESS_)/u.test(upper);
|
|
46
|
+
}
|
|
47
|
+
export async function readFxProfile(env) {
|
|
48
|
+
let file;
|
|
49
|
+
try {
|
|
50
|
+
file = await open(join(env.HOME || homedir(), ".fx/settings.json"), constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0));
|
|
51
|
+
const stat = await file.stat();
|
|
52
|
+
if (!stat.isFile() || stat.size > profileLimit)
|
|
53
|
+
throw new Error("Invalid profile size or type");
|
|
54
|
+
const buffer = Buffer.alloc(profileLimit + 1);
|
|
55
|
+
let length = 0;
|
|
56
|
+
while (length < buffer.length) {
|
|
57
|
+
const { bytesRead } = await file.read(buffer, length, buffer.length - length, length);
|
|
58
|
+
if (!bytesRead)
|
|
59
|
+
break;
|
|
60
|
+
length += bytesRead;
|
|
61
|
+
}
|
|
62
|
+
if (length > profileLimit)
|
|
63
|
+
throw new Error("Profile exceeds limit");
|
|
64
|
+
const bytes = buffer.subarray(0, length);
|
|
65
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
66
|
+
const settings = JSON.parse(text);
|
|
67
|
+
rejectDuplicateKeys(text);
|
|
68
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
69
|
+
throw new Error("Invalid profile shape");
|
|
70
|
+
return { settings: settings, fingerprint: createHash("sha256").update(bytes).digest("hex") };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.code === "ENOENT")
|
|
74
|
+
return { settings: {}, fingerprint: "missing" };
|
|
75
|
+
throw new AgentError("INVALID_CONFIG", "fx native settings must be a valid bounded JSON object in a regular profile file.");
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await file?.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** JSON.parse validates syntax first; this scan retains duplicate-key evidence. */
|
|
82
|
+
function rejectDuplicateKeys(text) {
|
|
83
|
+
const containers = [];
|
|
84
|
+
for (const match of text.matchAll(/"(?:\\.|[^"\\])*"|[{}\[\]]/gu)) {
|
|
85
|
+
const token = match[0];
|
|
86
|
+
if (token === "{")
|
|
87
|
+
containers.push(new Set());
|
|
88
|
+
else if (token === "[")
|
|
89
|
+
containers.push(undefined);
|
|
90
|
+
else if (token === "}" || token === "]")
|
|
91
|
+
containers.pop();
|
|
92
|
+
else if (/^\s*:/u.test(text.slice(match.index + token.length))) {
|
|
93
|
+
const keys = containers.at(-1);
|
|
94
|
+
const key = JSON.parse(token);
|
|
95
|
+
if (keys.has(key))
|
|
96
|
+
throw new Error("Duplicate native profile key");
|
|
97
|
+
keys.add(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=fx-profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fx-profile.js","sourceRoot":"","sources":["../../src/adapters/fx-profile.ts"],"names":[],"mappings":"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","sourcesContent":["import { constants } from \"node:fs\";\nimport { open } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { AgentError } from \"../errors.js\";\n\nconst profileLimit = 1024 * 1024;\n\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nexport function fxCustomConnection(settings: Record<string, unknown>, provider: string): { endpoint: string; keySlot: string } {\n if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(provider) || [\"gateway\", \"codex\", \"grok\"].includes(provider.toLowerCase())) {\n throw new AgentError(\"INVALID_CONFIG\", \"fx direct API keys require a valid native custom connection name.\");\n }\n const definitions = settings.providers;\n const selected = record(definitions) && Object.hasOwn(definitions, provider) ? definitions[provider] : undefined;\n if (!record(selected)) throw new AgentError(\"INVALID_CONFIG\", \"The selected fx custom connection is not configured in the native profile.\");\n if (selected.protocol !== \"openai-chat-completions\" || !record(selected.auth) || selected.auth.type !== \"bearer\") {\n throw new AgentError(\"UNSUPPORTED_OPTION\", \"fx direct API keys require a native Chat Completions connection with bearer environment authentication.\");\n }\n const slot = selected.auth.env;\n if (typeof slot !== \"string\" || !/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(slot) || unsafeCredentialSlot(slot)) {\n throw new AgentError(\"INVALID_CONFIG\", \"The fx custom connection credential slot cannot control the process, routing, or diagnostics.\");\n }\n const raw = selected.base_url;\n try {\n if (typeof raw !== \"string\" || raw.length > 2048 || /[^\\x21-\\x7e]|[\\\\?#]/u.test(raw) || !/^https?:\\/\\//iu.test(raw)\n || /^https?:\\/\\/[^/]*@/iu.test(raw)) throw new Error();\n const url = new URL(raw);\n if (!url.hostname || url.username || url.password || url.port === \"0\"\n || (url.protocol === \"http:\" && ![\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(url.hostname))) throw new Error();\n return { endpoint: raw.endsWith(\"/\") ? raw.slice(0, -1) : raw, keySlot: slot };\n } catch { throw new AgentError(\"INVALID_CONFIG\", \"The fx custom connection requires a valid HTTPS or loopback HTTP endpoint without credentials, query, or fragment.\"); }\n}\n\nfunction unsafeCredentialSlot(slot: string): boolean {\n const upper = slot.toUpperCase();\n return [\"HOME\", \"PATH\", \"SHELL\", \"ENV\", \"BASH_ENV\", \"ZDOTDIR\", \"PWD\", \"OLDPWD\", \"USER\", \"LOGNAME\", \"TMPDIR\", \"TMP\", \"TEMP\",\n \"AI_GATEWAY_API_KEY\", \"VERCEL_OIDC_TOKEN\", \"HTTP_PROXY\", \"HTTPS_PROXY\", \"ALL_PROXY\", \"NO_PROXY\"].includes(upper)\n || /^(?:FX_|NODE_|LD_|DYLD_|XDG_|SSL_|OPENSSL_|SUBHARNESS_)/u.test(upper);\n}\n\nexport async function readFxProfile(env: NodeJS.ProcessEnv): Promise<{ settings: Record<string, unknown>; fingerprint: string }> {\n let file;\n try {\n file = await open(join(env.HOME || homedir(), \".fx/settings.json\"), constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0));\n const stat = await file.stat();\n if (!stat.isFile() || stat.size > profileLimit) throw new Error(\"Invalid profile size or type\");\n const buffer = Buffer.alloc(profileLimit + 1);\n let length = 0;\n while (length < buffer.length) {\n const { bytesRead } = await file.read(buffer, length, buffer.length - length, length);\n if (!bytesRead) break;\n length += bytesRead;\n }\n if (length > profileLimit) throw new Error(\"Profile exceeds limit\");\n const bytes = buffer.subarray(0, length);\n const text = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n const settings: unknown = JSON.parse(text);\n rejectDuplicateKeys(text);\n if (!settings || typeof settings !== \"object\" || Array.isArray(settings)) throw new Error(\"Invalid profile shape\");\n return { settings: settings as Record<string, unknown>, fingerprint: createHash(\"sha256\").update(bytes).digest(\"hex\") };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return { settings: {}, fingerprint: \"missing\" };\n throw new AgentError(\"INVALID_CONFIG\", \"fx native settings must be a valid bounded JSON object in a regular profile file.\");\n } finally { await file?.close(); }\n}\n\n/** JSON.parse validates syntax first; this scan retains duplicate-key evidence. */\nfunction rejectDuplicateKeys(text: string): void {\n const containers: (Set<string> | undefined)[] = [];\n for (const match of text.matchAll(/\"(?:\\\\.|[^\"\\\\])*\"|[{}\\[\\]]/gu)) {\n const token = match[0];\n if (token === \"{\") containers.push(new Set());\n else if (token === \"[\") containers.push(undefined);\n else if (token === \"}\" || token === \"]\") containers.pop();\n else if (/^\\s*:/u.test(text.slice(match.index! + token.length))) {\n const keys = containers.at(-1)!;\n const key = JSON.parse(token) as string;\n if (keys.has(key)) throw new Error(\"Duplicate native profile key\");\n keys.add(key);\n }\n }\n}\n"]}
|
|
@@ -14,6 +14,7 @@ export declare class FxRpc {
|
|
|
14
14
|
private sequence;
|
|
15
15
|
private buffer;
|
|
16
16
|
private failure?;
|
|
17
|
+
private closing;
|
|
17
18
|
private readonly exited;
|
|
18
19
|
private failureHandler;
|
|
19
20
|
onMessage: (message: AcpEnvelope) => void;
|
|
@@ -26,4 +27,5 @@ export declare class FxRpc {
|
|
|
26
27
|
private read;
|
|
27
28
|
private invalid;
|
|
28
29
|
private fail;
|
|
30
|
+
private closedError;
|
|
29
31
|
}
|
package/dist/adapters/fx-rpc.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { AgentError } from "../errors.js";
|
|
3
|
+
import { processExitDescription } from "../process-diagnostics.js";
|
|
4
|
+
import { nativeRpcErrorDescription } from "./rpc.js";
|
|
3
5
|
/** A JSON-RPC 2.0 newline transport for fx's native ACP process. */
|
|
4
6
|
export class FxRpc {
|
|
5
7
|
child;
|
|
@@ -7,6 +9,7 @@ export class FxRpc {
|
|
|
7
9
|
sequence = 0;
|
|
8
10
|
buffer = "";
|
|
9
11
|
failure;
|
|
12
|
+
closing = false;
|
|
10
13
|
exited;
|
|
11
14
|
failureHandler = () => { };
|
|
12
15
|
onMessage = () => { };
|
|
@@ -15,16 +18,30 @@ export class FxRpc {
|
|
|
15
18
|
this.child.stdout.setEncoding("utf8");
|
|
16
19
|
this.child.stdout.on("data", chunk => this.read(chunk));
|
|
17
20
|
this.child.stderr.resume();
|
|
18
|
-
this.child.stdin.on("error", () =>
|
|
21
|
+
this.child.stdin.on("error", () => setImmediate(() => {
|
|
22
|
+
if (!this.closing && this.child.exitCode === null && this.child.signalCode === null) {
|
|
23
|
+
this.fail(new AgentError("HARNESS_FAILED", "The fx ACP input connection closed."));
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
19
26
|
this.child.on("error", () => this.fail(new AgentError("HARNESS_UNAVAILABLE", "Cannot start fx. Install fx 0.0.9 or a compatible release and make it available on PATH.")));
|
|
20
|
-
this.exited = new Promise(resolve => this.child.once("close", () => {
|
|
21
|
-
|
|
27
|
+
this.exited = new Promise(resolve => this.child.once("close", (code, signal) => {
|
|
28
|
+
if (this.closing) {
|
|
29
|
+
if (this.pending.size > 0)
|
|
30
|
+
this.fail(new AgentError("HARNESS_FAILED", "The fx session closed before the native operation completed."));
|
|
31
|
+
else
|
|
32
|
+
this.fail(this.closedError());
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.fail(new AgentError("HARNESS_FAILED", `fx exited before the native operation completed${processExitDescription(code, signal)}.`));
|
|
36
|
+
}
|
|
22
37
|
resolve();
|
|
23
38
|
}));
|
|
24
39
|
}
|
|
25
40
|
request(method, params) {
|
|
26
41
|
if (this.failure)
|
|
27
42
|
return Promise.reject(this.failure);
|
|
43
|
+
if (this.closing)
|
|
44
|
+
return Promise.reject(this.closedError());
|
|
28
45
|
const id = ++this.sequence;
|
|
29
46
|
return new Promise((resolve, reject) => {
|
|
30
47
|
this.pending.set(id, { method, resolve, reject });
|
|
@@ -34,6 +51,8 @@ export class FxRpc {
|
|
|
34
51
|
send(message) {
|
|
35
52
|
if (this.failure)
|
|
36
53
|
throw this.failure;
|
|
54
|
+
if (this.closing)
|
|
55
|
+
throw this.closedError();
|
|
37
56
|
this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`);
|
|
38
57
|
}
|
|
39
58
|
abort(error) {
|
|
@@ -48,6 +67,7 @@ export class FxRpc {
|
|
|
48
67
|
async close() {
|
|
49
68
|
if (this.child.exitCode !== null || this.child.signalCode !== null)
|
|
50
69
|
return this.exited;
|
|
70
|
+
this.closing = true;
|
|
51
71
|
this.child.kill("SIGTERM");
|
|
52
72
|
const timer = setTimeout(() => this.child.kill("SIGKILL"), 2_000);
|
|
53
73
|
timer.unref();
|
|
@@ -100,7 +120,10 @@ export class FxRpc {
|
|
|
100
120
|
const nativeCode = message.error?.code;
|
|
101
121
|
const unsupported = pending.method === "session/set_config_option" && (nativeCode === -32601 || nativeCode === -32602);
|
|
102
122
|
const code = unsupported ? "UNSUPPORTED_OPTION" : "NATIVE_ERROR";
|
|
103
|
-
|
|
123
|
+
const diagnostic = `fx rejected ${pending.method}${nativeRpcErrorDescription(message.error)}.`;
|
|
124
|
+
pending.reject(new AgentError(code, code === "UNSUPPORTED_OPTION"
|
|
125
|
+
? `${diagnostic} The requested session option is unsupported.`
|
|
126
|
+
: diagnostic));
|
|
104
127
|
}
|
|
105
128
|
else {
|
|
106
129
|
pending.resolve(message.result);
|
|
@@ -120,5 +143,8 @@ export class FxRpc {
|
|
|
120
143
|
this.pending.clear();
|
|
121
144
|
this.failureHandler(error);
|
|
122
145
|
}
|
|
146
|
+
closedError() {
|
|
147
|
+
return new AgentError("HARNESS_FAILED", "The fx session is closed.");
|
|
148
|
+
}
|
|
123
149
|
}
|
|
124
150
|
//# sourceMappingURL=fx-rpc.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fx-rpc.js","sourceRoot":"","sources":["../../src/adapters/fx-rpc.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport { AgentError } from \"../errors.js\";\n\nexport type AcpEnvelope = {\n jsonrpc?: string;\n id?: string | number;\n method?: string;\n params?: any;\n result?: any;\n error?: unknown;\n};\n\ntype Pending = { method: string; resolve(value: any): void; reject(error: Error): void };\n\n/** A JSON-RPC 2.0 newline transport for fx's native ACP process. */\nexport class FxRpc {\n private readonly child: ChildProcessWithoutNullStreams;\n private readonly pending = new Map<number, Pending>();\n private sequence = 0;\n private buffer = \"\";\n private failure?: AgentError;\n private readonly exited: Promise<void>;\n private failureHandler: (error: AgentError) => void = () => {};\n onMessage: (message: AcpEnvelope) => void = () => {};\n\n constructor(cwd: string, env: NodeJS.ProcessEnv, model?: string) {\n this.child = spawn(\"fx\", model ? [\"acp\", \"--model\", model] : [\"acp\"], { cwd, env, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n this.child.stdout.setEncoding(\"utf8\");\n this.child.stdout.on(\"data\", chunk => this.read(chunk));\n this.child.stderr.resume();\n this.child.stdin.on(\"error\", () => this.fail(new AgentError(\"HARNESS_FAILED\", \"The fx ACP input connection closed.\")));\n this.child.on(\"error\", () => this.fail(new AgentError(\"HARNESS_UNAVAILABLE\", \"Cannot start fx. Install fx 0.0.9 or a compatible release and make it available on PATH.\")));\n this.exited = new Promise(resolve => this.child.once(\"close\", () => {\n this.fail(new AgentError(\"HARNESS_FAILED\", \"fx exited before the native operation completed.\"));\n resolve();\n }));\n }\n\n request(method: string, params: unknown): Promise<any> {\n if (this.failure) return Promise.reject(this.failure);\n const id = ++this.sequence;\n return new Promise((resolve, reject) => {\n this.pending.set(id, { method, resolve, reject });\n this.send({ id, method, params });\n });\n }\n\n send(message: AcpEnvelope): void {\n if (this.failure) throw this.failure;\n this.child.stdin.write(`${JSON.stringify({ jsonrpc: \"2.0\", ...message })}\\n`);\n }\n\n abort(error: AgentError): void {\n this.fail(error);\n void this.close();\n }\n\n setFailureHandler(handler: (error: AgentError) => void): void {\n this.failureHandler = handler;\n if (this.failure) handler(this.failure);\n }\n\n async close(): Promise<void> {\n if (this.child.exitCode !== null || this.child.signalCode !== null) return this.exited;\n this.child.kill(\"SIGTERM\");\n const timer = setTimeout(() => this.child.kill(\"SIGKILL\"), 2_000);\n timer.unref();\n await this.exited;\n clearTimeout(timer);\n }\n\n private read(chunk: string): void {\n this.buffer += chunk;\n if (this.buffer.length > 16 * 1024 * 1024) return this.invalid();\n let newline: number;\n while ((newline = this.buffer.indexOf(\"\\n\")) >= 0) {\n const line = this.buffer.slice(0, newline);\n this.buffer = this.buffer.slice(newline + 1);\n if (!line.trim()) continue;\n let message: AcpEnvelope;\n try { message = JSON.parse(line); } catch { return this.invalid(); }\n if (!message || typeof message !== \"object\" || Array.isArray(message) || message.jsonrpc !== \"2.0\") return this.invalid();\n if (typeof message.method === \"string\") {\n if (Object.hasOwn(message, \"result\") || Object.hasOwn(message, \"error\")) return this.invalid();\n this.onMessage(message);\n continue;\n }\n if (typeof message.id !== \"number\") return this.invalid();\n const hasResult = Object.hasOwn(message, \"result\");\n const hasError = Object.hasOwn(message, \"error\");\n if (hasResult === hasError) return this.invalid();\n if (hasError) {\n const error = message.error;\n if (!error || typeof error !== \"object\" || Array.isArray(error)\n || !Number.isInteger((error as { code?: unknown }).code)\n || typeof (error as { message?: unknown }).message !== \"string\") return this.invalid();\n }\n const pending = this.pending.get(message.id);\n if (!pending) continue;\n this.pending.delete(message.id);\n if (hasError) {\n const nativeCode = (message.error as { code?: unknown } | null)?.code;\n const unsupported = pending.method === \"session/set_config_option\" && (nativeCode === -32601 || nativeCode === -32602);\n const code = unsupported ? \"UNSUPPORTED_OPTION\" : \"NATIVE_ERROR\";\n pending.reject(new AgentError(code, code === \"UNSUPPORTED_OPTION\" ? \"fx rejected the requested session option.\" : `fx rejected ${pending.method}. Check the native harness configuration.`));\n } else {\n pending.resolve(message.result);\n }\n }\n }\n\n private invalid(): void {\n this.fail(new AgentError(\"PROTOCOL_ERROR\", \"fx returned invalid ACP protocol data.\"));\n void this.close();\n }\n\n private fail(error: AgentError): void {\n if (this.failure) return;\n this.failure = error;\n for (const pending of this.pending.values()) pending.reject(error);\n this.pending.clear();\n this.failureHandler(error);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fx-rpc.js","sourceRoot":"","sources":["../../src/adapters/fx-rpc.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport { AgentError } from \"../errors.js\";\nimport { processExitDescription } from \"../process-diagnostics.js\";\nimport { nativeRpcErrorDescription } from \"./rpc.js\";\n\nexport type AcpEnvelope = {\n jsonrpc?: string;\n id?: string | number;\n method?: string;\n params?: any;\n result?: any;\n error?: unknown;\n};\n\ntype Pending = { method: string; resolve(value: any): void; reject(error: Error): void };\n\n/** A JSON-RPC 2.0 newline transport for fx's native ACP process. */\nexport class FxRpc {\n private readonly child: ChildProcessWithoutNullStreams;\n private readonly pending = new Map<number, Pending>();\n private sequence = 0;\n private buffer = \"\";\n private failure?: AgentError;\n private closing = false;\n private readonly exited: Promise<void>;\n private failureHandler: (error: AgentError) => void = () => {};\n onMessage: (message: AcpEnvelope) => void = () => {};\n\n constructor(cwd: string, env: NodeJS.ProcessEnv, model?: string) {\n this.child = spawn(\"fx\", model ? [\"acp\", \"--model\", model] : [\"acp\"], { cwd, env, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n this.child.stdout.setEncoding(\"utf8\");\n this.child.stdout.on(\"data\", chunk => this.read(chunk));\n this.child.stderr.resume();\n this.child.stdin.on(\"error\", () => setImmediate(() => {\n if (!this.closing && this.child.exitCode === null && this.child.signalCode === null) {\n this.fail(new AgentError(\"HARNESS_FAILED\", \"The fx ACP input connection closed.\"));\n }\n }));\n this.child.on(\"error\", () => this.fail(new AgentError(\"HARNESS_UNAVAILABLE\", \"Cannot start fx. Install fx 0.0.9 or a compatible release and make it available on PATH.\")));\n this.exited = new Promise(resolve => this.child.once(\"close\", (code, signal) => {\n if (this.closing) {\n if (this.pending.size > 0) this.fail(new AgentError(\"HARNESS_FAILED\",\"The fx session closed before the native operation completed.\"));\n else this.fail(this.closedError());\n } else {\n this.fail(new AgentError(\"HARNESS_FAILED\",\n `fx exited before the native operation completed${processExitDescription(code, signal)}.`));\n }\n resolve();\n }));\n }\n\n request(method: string, params: unknown): Promise<any> {\n if (this.failure) return Promise.reject(this.failure);\n if (this.closing) return Promise.reject(this.closedError());\n const id = ++this.sequence;\n return new Promise((resolve, reject) => {\n this.pending.set(id, { method, resolve, reject });\n this.send({ id, method, params });\n });\n }\n\n send(message: AcpEnvelope): void {\n if (this.failure) throw this.failure;\n if (this.closing) throw this.closedError();\n this.child.stdin.write(`${JSON.stringify({ jsonrpc: \"2.0\", ...message })}\\n`);\n }\n\n abort(error: AgentError): void {\n this.fail(error);\n void this.close();\n }\n\n setFailureHandler(handler: (error: AgentError) => void): void {\n this.failureHandler = handler;\n if (this.failure) handler(this.failure);\n }\n\n async close(): Promise<void> {\n if (this.child.exitCode !== null || this.child.signalCode !== null) return this.exited;\n this.closing = true;\n this.child.kill(\"SIGTERM\");\n const timer = setTimeout(() => this.child.kill(\"SIGKILL\"), 2_000);\n timer.unref();\n await this.exited;\n clearTimeout(timer);\n }\n\n private read(chunk: string): void {\n this.buffer += chunk;\n if (this.buffer.length > 16 * 1024 * 1024) return this.invalid();\n let newline: number;\n while ((newline = this.buffer.indexOf(\"\\n\")) >= 0) {\n const line = this.buffer.slice(0, newline);\n this.buffer = this.buffer.slice(newline + 1);\n if (!line.trim()) continue;\n let message: AcpEnvelope;\n try { message = JSON.parse(line); } catch { return this.invalid(); }\n if (!message || typeof message !== \"object\" || Array.isArray(message) || message.jsonrpc !== \"2.0\") return this.invalid();\n if (typeof message.method === \"string\") {\n if (Object.hasOwn(message, \"result\") || Object.hasOwn(message, \"error\")) return this.invalid();\n this.onMessage(message);\n continue;\n }\n if (typeof message.id !== \"number\") return this.invalid();\n const hasResult = Object.hasOwn(message, \"result\");\n const hasError = Object.hasOwn(message, \"error\");\n if (hasResult === hasError) return this.invalid();\n if (hasError) {\n const error = message.error;\n if (!error || typeof error !== \"object\" || Array.isArray(error)\n || !Number.isInteger((error as { code?: unknown }).code)\n || typeof (error as { message?: unknown }).message !== \"string\") return this.invalid();\n }\n const pending = this.pending.get(message.id);\n if (!pending) continue;\n this.pending.delete(message.id);\n if (hasError) {\n const nativeCode = (message.error as { code?: unknown } | null)?.code;\n const unsupported = pending.method === \"session/set_config_option\" && (nativeCode === -32601 || nativeCode === -32602);\n const code = unsupported ? \"UNSUPPORTED_OPTION\" : \"NATIVE_ERROR\";\n const diagnostic = `fx rejected ${pending.method}${nativeRpcErrorDescription(message.error)}.`;\n pending.reject(new AgentError(code, code === \"UNSUPPORTED_OPTION\"\n ? `${diagnostic} The requested session option is unsupported.`\n : diagnostic));\n } else {\n pending.resolve(message.result);\n }\n }\n }\n\n private invalid(): void {\n this.fail(new AgentError(\"PROTOCOL_ERROR\", \"fx returned invalid ACP protocol data.\"));\n void this.close();\n }\n\n private fail(error: AgentError): void {\n if (this.failure) return;\n this.failure = error;\n for (const pending of this.pending.values()) pending.reject(error);\n this.pending.clear();\n this.failureHandler(error);\n }\n\n private closedError(): AgentError {\n return new AgentError(\"HARNESS_FAILED\", \"The fx session is closed.\");\n }\n}\n"]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { AgentError } from "../errors.js";
|
|
3
|
+
const outputLimit = 1024 * 1024;
|
|
4
|
+
const ownsGroup = process.platform !== "win32";
|
|
5
|
+
/** Credential diagnostics never release ownership on a timeout or unknown exit. */
|
|
6
|
+
export async function captureFxStatus(cwd, env) {
|
|
7
|
+
const child = (() => {
|
|
8
|
+
try {
|
|
9
|
+
return spawn("fx", ["status", "--json"], { cwd, env, detached: ownsGroup, stdio: ["ignore", "pipe", "pipe"] });
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new AgentError("INVALID_CONFIG", "fx credential-source verification could not start with the selected process configuration.");
|
|
13
|
+
}
|
|
14
|
+
})();
|
|
15
|
+
let failure;
|
|
16
|
+
let exitCode = null;
|
|
17
|
+
let exitSeen = false;
|
|
18
|
+
let bytes = 0;
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let resolveExited;
|
|
21
|
+
let rejectRetirement;
|
|
22
|
+
const exited = new Promise(resolve => { resolveExited = resolve; });
|
|
23
|
+
const unconfirmed = new Promise((_resolve, reject) => { rejectRetirement = reject; });
|
|
24
|
+
const closed = new Promise(resolve => child.once("close", resolve));
|
|
25
|
+
let killTimer;
|
|
26
|
+
let retirementTimer;
|
|
27
|
+
const signal = (value) => {
|
|
28
|
+
if (child.pid === undefined)
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
if (ownsGroup)
|
|
32
|
+
process.kill(-child.pid, value);
|
|
33
|
+
else if (!exitSeen)
|
|
34
|
+
child.kill(value);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code !== "ESRCH")
|
|
38
|
+
failure = new AgentError("CANCELLATION_FAILED", "fx credential-source verification could not retire its owned process.");
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const stop = (error) => {
|
|
42
|
+
if (failure)
|
|
43
|
+
return;
|
|
44
|
+
failure = error;
|
|
45
|
+
signal("SIGTERM");
|
|
46
|
+
killTimer = setTimeout(() => signal("SIGKILL"), 250);
|
|
47
|
+
retirementTimer = setTimeout(() => rejectRetirement(new AgentError("CANCELLATION_FAILED", "fx credential-source verification did not confirm process termination.")), 1_250);
|
|
48
|
+
};
|
|
49
|
+
child.stdout.on("data", (chunk) => {
|
|
50
|
+
if (failure)
|
|
51
|
+
return;
|
|
52
|
+
bytes += chunk.byteLength;
|
|
53
|
+
if (bytes > outputLimit)
|
|
54
|
+
stop(new AgentError("PROTOCOL_ERROR", "fx returned oversized credential-source status."));
|
|
55
|
+
else
|
|
56
|
+
chunks.push(chunk);
|
|
57
|
+
});
|
|
58
|
+
child.stderr.resume();
|
|
59
|
+
child.once("error", error => {
|
|
60
|
+
const absent = error.code === "ENOENT";
|
|
61
|
+
stop(new AgentError(absent ? "HARNESS_UNAVAILABLE" : "PROTOCOL_ERROR", absent
|
|
62
|
+
? "Cannot start fx. Install a compatible fx release and make it available on PATH."
|
|
63
|
+
: "fx credential-source verification failed to communicate with the native process."));
|
|
64
|
+
if (child.pid === undefined)
|
|
65
|
+
resolveExited();
|
|
66
|
+
});
|
|
67
|
+
child.once("exit", code => { exitSeen = true; exitCode = code; resolveExited(); });
|
|
68
|
+
const timer = setTimeout(() => stop(new AgentError("PROTOCOL_ERROR", "fx credential-source verification timed out.")), 2_000);
|
|
69
|
+
try {
|
|
70
|
+
await Promise.race([exited, unconfirmed]);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
clearTimeout(killTimer);
|
|
75
|
+
clearTimeout(retirementTimer);
|
|
76
|
+
signal("SIGKILL");
|
|
77
|
+
await retireStreams(child, closed);
|
|
78
|
+
}
|
|
79
|
+
if (failure)
|
|
80
|
+
throw failure;
|
|
81
|
+
if (exitCode !== 0)
|
|
82
|
+
throw new AgentError("PROTOCOL_ERROR", "fx credential-source verification exited without usable native status.");
|
|
83
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
84
|
+
}
|
|
85
|
+
async function retireStreams(child, closed) {
|
|
86
|
+
let timer;
|
|
87
|
+
try {
|
|
88
|
+
await Promise.race([closed, new Promise(resolve => {
|
|
89
|
+
timer = setTimeout(() => { child.stdout.destroy(); child.stderr.destroy(); resolve(); }, 250);
|
|
90
|
+
})]);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=fx-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fx-status.js","sourceRoot":"","sources":["../../src/adapters/fx-status.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport { AgentError } from \"../errors.js\";\n\nconst outputLimit = 1024 * 1024;\nconst ownsGroup = process.platform !== \"win32\";\n\n/** Credential diagnostics never release ownership on a timeout or unknown exit. */\nexport async function captureFxStatus(cwd: string, env: NodeJS.ProcessEnv): Promise<string> {\n const child = (() => {\n try { return spawn(\"fx\", [\"status\", \"--json\"], { cwd, env, detached: ownsGroup, stdio: [\"ignore\", \"pipe\", \"pipe\"] }); }\n catch { throw new AgentError(\"INVALID_CONFIG\", \"fx credential-source verification could not start with the selected process configuration.\"); }\n })();\n let failure: AgentError | undefined;\n let exitCode: number | null = null;\n let exitSeen = false;\n let bytes = 0;\n const chunks: Buffer[] = [];\n let resolveExited!: () => void;\n let rejectRetirement!: (error: AgentError) => void;\n const exited = new Promise<void>(resolve => { resolveExited = resolve; });\n const unconfirmed = new Promise<never>((_resolve, reject) => { rejectRetirement = reject; });\n const closed = new Promise<void>(resolve => child.once(\"close\", resolve));\n let killTimer: NodeJS.Timeout | undefined;\n let retirementTimer: NodeJS.Timeout | undefined;\n\n const signal = (value: NodeJS.Signals) => {\n if (child.pid === undefined) return;\n try {\n if (ownsGroup) process.kill(-child.pid, value);\n else if (!exitSeen) child.kill(value);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ESRCH\") failure = new AgentError(\"CANCELLATION_FAILED\", \"fx credential-source verification could not retire its owned process.\");\n }\n };\n const stop = (error: AgentError) => {\n if (failure) return;\n failure = error;\n signal(\"SIGTERM\");\n killTimer = setTimeout(() => signal(\"SIGKILL\"), 250);\n retirementTimer = setTimeout(() => rejectRetirement(new AgentError(\"CANCELLATION_FAILED\", \"fx credential-source verification did not confirm process termination.\")), 1_250);\n };\n child.stdout.on(\"data\", (chunk: Buffer) => {\n if (failure) return;\n bytes += chunk.byteLength;\n if (bytes > outputLimit) stop(new AgentError(\"PROTOCOL_ERROR\", \"fx returned oversized credential-source status.\"));\n else chunks.push(chunk);\n });\n child.stderr.resume();\n child.once(\"error\", error => {\n const absent = (error as NodeJS.ErrnoException).code === \"ENOENT\";\n stop(new AgentError(absent ? \"HARNESS_UNAVAILABLE\" : \"PROTOCOL_ERROR\", absent\n ? \"Cannot start fx. Install a compatible fx release and make it available on PATH.\"\n : \"fx credential-source verification failed to communicate with the native process.\"));\n if (child.pid === undefined) resolveExited();\n });\n child.once(\"exit\", code => { exitSeen = true; exitCode = code; resolveExited(); });\n const timer = setTimeout(() => stop(new AgentError(\"PROTOCOL_ERROR\", \"fx credential-source verification timed out.\")), 2_000);\n try { await Promise.race([exited, unconfirmed]); }\n finally {\n clearTimeout(timer);\n clearTimeout(killTimer);\n clearTimeout(retirementTimer);\n signal(\"SIGKILL\");\n await retireStreams(child, closed);\n }\n if (failure) throw failure;\n if (exitCode !== 0) throw new AgentError(\"PROTOCOL_ERROR\", \"fx credential-source verification exited without usable native status.\");\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n\nasync function retireStreams(child: Pick<ChildProcessWithoutNullStreams, \"stdout\" | \"stderr\">, closed: Promise<void>): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([closed, new Promise<void>(resolve => {\n timer = setTimeout(() => { child.stdout.destroy(); child.stderr.destroy(); resolve(); }, 250);\n })]);\n } finally { clearTimeout(timer); }\n}\n"]}
|
package/dist/adapters/fx.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import { NativeApprovals } from "./approval-lifetime.js";
|
|
2
|
+
import { fxApproval } from "./fx-approvals.js";
|
|
1
3
|
import { AgentError } from "../errors.js";
|
|
2
|
-
import { fxEnvironment, fxSettingsFingerprint, verifyFxAccess } from "./fx-auth.js";
|
|
4
|
+
import { fxAccessRoute, fxEnvironment, fxSettingsFingerprint, verifyFxAccess } from "./fx-auth.js";
|
|
3
5
|
import { FxRpc } from "./fx-rpc.js";
|
|
4
6
|
import { verifyFxPermissionMode } from "./fx-permissions.js";
|
|
5
7
|
import { startFxTools } from "./fx-tools.js";
|
|
6
8
|
export async function openFx(options, createRpc = (cwd, env, model) => new FxRpc(cwd, env, model)) {
|
|
7
9
|
if (options.harness.kind !== "fx")
|
|
8
10
|
throw new AgentError("INVALID_CONFIG", "fx received configuration for a different harness.");
|
|
9
|
-
const
|
|
10
|
-
const baseline = await fxSettingsFingerprint(env);
|
|
11
|
-
|
|
11
|
+
const route = await fxAccessRoute(options);
|
|
12
|
+
const baseline = route.settingsFingerprint ?? await fxSettingsFingerprint(options.env);
|
|
13
|
+
const env = fxEnvironment(options, route);
|
|
14
|
+
await verifyFxAccess(options.cwd, env, route, options.harness.model);
|
|
12
15
|
await verifyFxPermissionMode(options.cwd, env, options.harness.permissionMode);
|
|
16
|
+
if (await fxSettingsFingerprint(env) !== baseline)
|
|
17
|
+
throw new AgentError("INVALID_CONFIG", "fx native settings changed during access verification. Start a new session.");
|
|
13
18
|
const tools = await startFxTools(options.agent);
|
|
14
19
|
let rpc;
|
|
15
20
|
try {
|
|
@@ -31,17 +36,17 @@ export async function openFx(options, createRpc = (cwd, env, model) => new FxRpc
|
|
|
31
36
|
});
|
|
32
37
|
if (typeof created?.sessionId !== "string" || created.sessionId.length === 0)
|
|
33
38
|
throw new AgentError("PROTOCOL_ERROR", "fx did not return a native session identifier.");
|
|
34
|
-
const selectedModel = verifySelection(created.configOptions, options.harness.model);
|
|
39
|
+
const selectedModel = verifySelection(created.configOptions, route.provider, options.harness.model);
|
|
35
40
|
if (options.harness.effort !== undefined) {
|
|
36
41
|
verifyEffortAvailable(created.configOptions, options.harness.effort);
|
|
37
42
|
const configured = await startedRpc.request("session/set_config_option", { sessionId: created.sessionId, configId: "effort", value: options.harness.effort });
|
|
38
|
-
verifySelection(configured?.configOptions, selectedModel, options.harness.effort);
|
|
43
|
+
verifySelection(configured?.configOptions, route.provider, selectedModel, options.harness.effort);
|
|
39
44
|
}
|
|
40
45
|
if (await fxSettingsFingerprint(env) !== baseline)
|
|
41
46
|
throw new AgentError("INVALID_CONFIG", "fx native settings changed during session startup. Start a new session.");
|
|
42
47
|
if (startupFailure)
|
|
43
48
|
throw startupFailure;
|
|
44
|
-
return new FxSession(startedRpc, created.sessionId, options, env, baseline, tools);
|
|
49
|
+
return new FxSession(startedRpc, created.sessionId, options, env, baseline, tools, route.provider, selectedModel);
|
|
45
50
|
}
|
|
46
51
|
catch (error) {
|
|
47
52
|
await rpc?.close();
|
|
@@ -65,21 +70,21 @@ function option(options, id) {
|
|
|
65
70
|
throw new AgentError("PROTOCOL_ERROR", "fx returned invalid native session options.");
|
|
66
71
|
return options.find(value => value && typeof value === "object" && value.id === id);
|
|
67
72
|
}
|
|
68
|
-
function verifySelection(configOptions, requestedModel, effort) {
|
|
73
|
+
function verifySelection(configOptions, requestedProvider, requestedModel, effort) {
|
|
69
74
|
const provider = option(configOptions, "provider");
|
|
70
75
|
const selectedModel = option(configOptions, "model");
|
|
71
76
|
if (!provider || !selectedModel) {
|
|
72
77
|
throw new AgentError("PROTOCOL_ERROR", "fx did not expose compatible native provider and model selectors.");
|
|
73
78
|
}
|
|
74
|
-
if (provider.currentValue !==
|
|
75
|
-
throw new AgentError("INVALID_CONFIG", "fx selected a provider outside the authorized
|
|
79
|
+
if (provider.currentValue !== requestedProvider) {
|
|
80
|
+
throw new AgentError("INVALID_CONFIG", "fx selected a provider outside the authorized route.");
|
|
76
81
|
}
|
|
77
82
|
const model = selectedModel.currentValue;
|
|
78
83
|
if (typeof model !== "string" || model.length === 0) {
|
|
79
84
|
throw new AgentError("INVALID_CONFIG", "fx could not select a verifiable native default for this connection. Supply --model <model>.");
|
|
80
85
|
}
|
|
81
86
|
if (requestedModel && model !== requestedModel) {
|
|
82
|
-
throw new AgentError("INVALID_CONFIG", "fx changed the explicitly selected
|
|
87
|
+
throw new AgentError("INVALID_CONFIG", "fx changed the explicitly selected model.");
|
|
83
88
|
}
|
|
84
89
|
if (effort !== undefined && option(configOptions, "effort")?.currentValue !== effort) {
|
|
85
90
|
throw new AgentError("INVALID_CONFIG", "fx did not apply the explicitly selected effort.");
|
|
@@ -99,21 +104,26 @@ class FxSession {
|
|
|
99
104
|
env;
|
|
100
105
|
settingsFingerprint;
|
|
101
106
|
tools;
|
|
107
|
+
selectedProvider;
|
|
108
|
+
selectedModel;
|
|
102
109
|
active;
|
|
103
110
|
failure;
|
|
104
111
|
retirement;
|
|
105
112
|
first = true;
|
|
106
113
|
closed = false;
|
|
107
|
-
constructor(rpc, sessionId, options, env, settingsFingerprint, tools) {
|
|
114
|
+
constructor(rpc, sessionId, options, env, settingsFingerprint, tools, selectedProvider, selectedModel) {
|
|
108
115
|
this.rpc = rpc;
|
|
109
116
|
this.sessionId = sessionId;
|
|
110
117
|
this.options = options;
|
|
111
118
|
this.env = env;
|
|
112
119
|
this.settingsFingerprint = settingsFingerprint;
|
|
113
120
|
this.tools = tools;
|
|
121
|
+
this.selectedProvider = selectedProvider;
|
|
122
|
+
this.selectedModel = selectedModel;
|
|
114
123
|
rpc.setFailureHandler(error => {
|
|
115
124
|
this.failure ??= error;
|
|
116
125
|
const active = this.active;
|
|
126
|
+
active?.approvals.clear();
|
|
117
127
|
this.retirement ??= this.rpc.close();
|
|
118
128
|
if (active)
|
|
119
129
|
void this.retirement.then(() => this.finish(active, undefined, error));
|
|
@@ -123,31 +133,42 @@ class FxSession {
|
|
|
123
133
|
message(message) {
|
|
124
134
|
const active = this.active;
|
|
125
135
|
const params = message.params;
|
|
136
|
+
if (message.method === "$/cancel_request" && active && (typeof params?.requestId === "string" || typeof params?.requestId === "number")) {
|
|
137
|
+
active.withdrawnApprovals.add(params.requestId);
|
|
138
|
+
active.approvals.withdraw(params.requestId);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
126
141
|
if (message.id !== undefined) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
catch { /* cancellation will surface the transport failure */ }
|
|
131
|
-
if (!active) {
|
|
132
|
-
this.rpc.abort(new AgentError("INPUT_REQUIRED", "fx requested interactive input outside an active task."));
|
|
133
|
-
}
|
|
134
|
-
else if (!active.failure) {
|
|
135
|
-
active.failure = new AgentError("INPUT_REQUIRED", "fx requested interactive input or approval. Configure native permissions before retrying; no approval was granted.");
|
|
136
|
-
this.tools.stopAdmission();
|
|
142
|
+
if (active && active.submitted && !active.terminal && !active.cancelRequested && !active.failure && params?.sessionId === this.sessionId
|
|
143
|
+
&& message.method === "session/request_permission" && this.options.onApproval) {
|
|
137
144
|
try {
|
|
138
|
-
|
|
145
|
+
const request = fxApproval(params);
|
|
146
|
+
void active.approvals.request(message.id, request, this.options.onApproval).then(content => {
|
|
147
|
+
if (this.active === active && !active.terminal && !active.cancelRequested && !active.failure && !active.withdrawnApprovals.has(message.id)) {
|
|
148
|
+
this.rpc.send({ id: message.id, result: { outcome: { outcome: "selected", optionId: content.decision } } });
|
|
149
|
+
}
|
|
150
|
+
}).catch(error => {
|
|
151
|
+
if (!(error instanceof AgentError && error.code === "REQUEST_EXPIRED"))
|
|
152
|
+
this.unsupportedRequest(message, active);
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
139
155
|
}
|
|
140
|
-
catch { /*
|
|
141
|
-
active.cancellationTimer = setTimeout(() => {
|
|
142
|
-
if (this.active === active && !active.terminal)
|
|
143
|
-
this.rpc.abort(new AgentError("CANCELLATION_FAILED", "fx did not stop after requesting interactive input."));
|
|
144
|
-
}, 2_000);
|
|
145
|
-
active.cancellationTimer.unref();
|
|
156
|
+
catch { /* Unsupported requests retain the explicit input failure path. */ }
|
|
146
157
|
}
|
|
158
|
+
this.unsupportedRequest(message, active);
|
|
147
159
|
return;
|
|
148
160
|
}
|
|
149
161
|
if (params?.sessionId !== this.sessionId)
|
|
150
162
|
return;
|
|
163
|
+
if (message.method === "session/update" && params.update?.sessionUpdate === "config_option_update") {
|
|
164
|
+
try {
|
|
165
|
+
verifySelection(params.update.configOptions, this.selectedProvider, this.selectedModel, this.options.harness.effort);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
this.rpc.abort(error);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
151
172
|
if (!active || message.method !== "session/update")
|
|
152
173
|
return;
|
|
153
174
|
const update = params.update;
|
|
@@ -173,10 +194,34 @@ class FxSession {
|
|
|
173
194
|
else
|
|
174
195
|
active.text += update.content.text;
|
|
175
196
|
}
|
|
197
|
+
unsupportedRequest(message, active) {
|
|
198
|
+
try {
|
|
199
|
+
this.rpc.send({ id: message.id, result: { outcome: { outcome: "cancelled" } } });
|
|
200
|
+
}
|
|
201
|
+
catch { /* cancellation will surface the transport failure */ }
|
|
202
|
+
if (!active) {
|
|
203
|
+
this.rpc.abort(new AgentError("INPUT_REQUIRED", "fx requested interactive input outside an active task."));
|
|
204
|
+
}
|
|
205
|
+
else if (!active.failure) {
|
|
206
|
+
active.failure = new AgentError("INPUT_REQUIRED", "fx requested interactive input or approval. Configure native permissions before retrying; no approval was granted.");
|
|
207
|
+
active.approvals.clear();
|
|
208
|
+
this.tools.stopAdmission();
|
|
209
|
+
try {
|
|
210
|
+
this.rpc.send({ method: "session/cancel", params: { sessionId: this.sessionId } });
|
|
211
|
+
}
|
|
212
|
+
catch { /* onFailure settles the turn */ }
|
|
213
|
+
active.cancellationTimer = setTimeout(() => {
|
|
214
|
+
if (this.active === active && !active.terminal)
|
|
215
|
+
this.rpc.abort(new AgentError("CANCELLATION_FAILED", "fx did not stop after requesting interactive input."));
|
|
216
|
+
}, 2_000);
|
|
217
|
+
active.cancellationTimer.unref();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
176
220
|
async finish(active, result, failure) {
|
|
177
221
|
if (active.terminal)
|
|
178
222
|
return;
|
|
179
223
|
active.terminal = true;
|
|
224
|
+
active.approvals.clear();
|
|
180
225
|
active.resolveNativeStopped();
|
|
181
226
|
if (active.cancellationTimer)
|
|
182
227
|
clearTimeout(active.cancellationTimer);
|
|
@@ -208,7 +253,7 @@ class FxSession {
|
|
|
208
253
|
const completion = new Promise((a, b) => { resolve = a; reject = b; });
|
|
209
254
|
const admission = new Promise(resolveAdmission => { releaseAdmission = resolveAdmission; });
|
|
210
255
|
const nativeStopped = new Promise(resolveStopped => { resolveNativeStopped = resolveStopped; });
|
|
211
|
-
const active = { text: "", responseBytes: 0, responseTooLarge: false, completion, resolve, reject, terminal: false, submitted: false, cancelRequested: false, admission, releaseAdmission, nativeStopped, resolveNativeStopped };
|
|
256
|
+
const active = { approvals: new NativeApprovals(), withdrawnApprovals: new Set(), text: "", responseBytes: 0, responseTooLarge: false, completion, resolve, reject, terminal: false, submitted: false, cancelRequested: false, admission, releaseAdmission, nativeStopped, resolveNativeStopped };
|
|
212
257
|
this.active = active;
|
|
213
258
|
void completion.catch(() => { });
|
|
214
259
|
try {
|
|
@@ -248,6 +293,7 @@ class FxSession {
|
|
|
248
293
|
if (!active)
|
|
249
294
|
return;
|
|
250
295
|
active.cancelRequested = true;
|
|
296
|
+
active.approvals.clear();
|
|
251
297
|
this.tools.stopAdmission();
|
|
252
298
|
await bounded(active.admission, 2_000);
|
|
253
299
|
const admittedTools = this.tools.drain();
|
|
@@ -276,6 +322,7 @@ class FxSession {
|
|
|
276
322
|
try {
|
|
277
323
|
if (this.active) {
|
|
278
324
|
this.active.cancelRequested = true;
|
|
325
|
+
this.active.approvals.clear();
|
|
279
326
|
await bounded(this.active.admission, 2_000);
|
|
280
327
|
if (this.active.submitted && !this.active.terminal)
|
|
281
328
|
this.rpc.send({ method: "session/cancel", params: { sessionId: this.sessionId } });
|