agents-can-communicate 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -91
- package/SECURITY.md +8 -5
- package/bin/acc-bootstrap.mjs +5 -54
- package/bin/acc-claude-channel.mjs +8 -175
- package/bin/acc-hook.mjs +4 -133
- package/bin/acc-mcp.mjs +4 -59
- package/bin/acc-update-worker.mjs +8 -0
- package/bin/acc.mjs +5 -39
- package/bin/entrypoints/acc-bootstrap.mjs +47 -0
- package/bin/entrypoints/acc-claude-channel.mjs +149 -0
- package/bin/entrypoints/acc-hook.mjs +58 -0
- package/bin/entrypoints/acc-mcp.mjs +70 -0
- package/bin/entrypoints/acc.mjs +51 -0
- package/bin/entrypoints/claude-channel-stdio.mjs +29 -0
- package/bin/entrypoints/hook-output.mjs +73 -0
- package/docs/ADAPTER_AUTHORING.md +62 -24
- package/docs/ARCHITECTURE.md +79 -20
- package/docs/CAPABILITIES.md +52 -30
- package/docs/CLI.md +164 -35
- package/docs/CONCEPTS.md +97 -93
- package/docs/CONFIGURATION.md +66 -45
- package/docs/DESIGN_DECISIONS.md +24 -15
- package/docs/GETTING_STARTED.md +124 -118
- package/docs/GLOSSARY.md +12 -6
- package/docs/HOW_IT_WORKS.md +136 -229
- package/docs/MCP.md +98 -31
- package/docs/PROTOCOL.md +76 -14
- package/docs/RELEASING.md +128 -86
- package/docs/SECURITY_MODEL.md +52 -15
- package/docs/TROUBLESHOOTING.md +88 -18
- package/docs/UPGRADING.md +119 -0
- package/docs/WHY_ACC.md +19 -16
- package/docs/index.md +40 -24
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +112 -9
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +6 -6
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -13
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +220 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport-evidence.json +259 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport.json +21 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport-evidence.json +259 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport.json +21 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +14 -2
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +111 -8
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +26 -44
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +70 -17
- package/node_modules/@agents-can-communicate/adapter-codex/src/config-block.mjs +231 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +71 -63
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +107 -103
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +96 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +3 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +111 -8
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +111 -8
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +111 -8
- package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +5 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +26 -4
- package/node_modules/@agents-can-communicate/adapter-sdk/src/decision-text.mjs +19 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +5 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +9 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -6
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +29 -9
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +8 -6
- package/node_modules/@agents-can-communicate/cli/package.json +4 -2
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +49 -22
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +11 -4
- package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +9 -3
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +109 -14
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +66 -18
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +35 -12
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +110 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command.mjs +63 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +71 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +110 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +167 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +73 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +60 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +60 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/location.mjs +55 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +145 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/policy.mjs +4 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +40 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +21 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +122 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
- package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +33 -108
- package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +25 -7
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +3 -0
- package/node_modules/@agents-can-communicate/core/src/claims.mjs +13 -8
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +48 -24
- package/node_modules/@agents-can-communicate/core/src/decision-changes.mjs +36 -0
- package/node_modules/@agents-can-communicate/core/src/decision-state.mjs +42 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +11 -4
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +48 -17
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +35 -15
- package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +7 -5
- package/node_modules/@agents-can-communicate/core/src/message-pages.mjs +75 -0
- package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +31 -0
- package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +128 -0
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +58 -132
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +31 -2
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +38 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +86 -10
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +1 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -4
- package/node_modules/@agents-can-communicate/hook-runner/src/native-retirement.mjs +48 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +175 -67
- package/node_modules/@agents-can-communicate/hook-runner/src/session-lifecycle.mjs +13 -0
- package/node_modules/@agents-can-communicate/installer/package.json +4 -2
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +17 -2
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +11 -5
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +1 -1
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +21 -0
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +20 -6
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +10 -4
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +3 -3
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/protocol.mjs +85 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +3 -2
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +59 -57
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +47 -33
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +3 -0
- package/node_modules/@agents-can-communicate/protocol/src/decision-changes.mjs +15 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +3 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +14 -5
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +5 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/deadline.mjs +7 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +2 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +25 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +28 -25
- package/package.json +2 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { realpath, stat } from "node:fs/promises";
|
|
3
4
|
|
|
4
5
|
import { openWebSocketPeer } from "./ws-json-rpc.mjs";
|
|
5
6
|
|
|
6
7
|
// The Codex App Server queue protocol, captured on codex-cli 0.152.1. Every
|
|
7
8
|
// method here is official and present in the generated schema: initialize,
|
|
8
|
-
// thread/loaded/list, thread/list, thread/
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// escapes to core.
|
|
9
|
+
// thread/loaded/list, thread/list, metadata-only thread/read, thread/queue/list,
|
|
10
|
+
// thread/queue/add. Thread history is never requested; metadata reads explicitly
|
|
11
|
+
// exclude turns. Closed safe results only; no vendor string escapes to core.
|
|
12
12
|
|
|
13
13
|
export const PROTOCOL_CONTRACT = "codex-app-server-thread-queue-v1";
|
|
14
14
|
export const MINIMUM_VERSION = "0.152.1";
|
|
@@ -42,7 +42,7 @@ export function isMethodMissing(error) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export function openCodexAppServer({ socketPath, timeoutMs = 5_000 }) {
|
|
45
|
-
return openWebSocketPeer({ socketPath, timeoutMs });
|
|
45
|
+
return openWebSocketPeer({ socketPath, timeoutMs, retainNotifications: false });
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export async function initializeCodex(peer) {
|
|
@@ -52,7 +52,7 @@ export async function initializeCodex(peer) {
|
|
|
52
52
|
return serverVersionOf(initialized?.userAgent);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export async function probeCodexQueue(peer, { threadId, minimum = MINIMUM_VERSION }) {
|
|
55
|
+
export async function probeCodexQueue(peer, { threadId, minimum = MINIMUM_VERSION } = {}) {
|
|
56
56
|
const serverVersion = await initializeCodex(peer);
|
|
57
57
|
if (serverVersion === null || parseStableVersion(serverVersion) === null) {
|
|
58
58
|
return { supported: false, serverVersion, reasonCode: "prerelease_not_captured" };
|
|
@@ -61,9 +61,16 @@ export async function probeCodexQueue(peer, { threadId, minimum = MINIMUM_VERSIO
|
|
|
61
61
|
return { supported: false, serverVersion, reasonCode: "below_minimum_version" };
|
|
62
62
|
}
|
|
63
63
|
try {
|
|
64
|
-
|
|
64
|
+
if (threadId === undefined) {
|
|
65
|
+
const loaded = await pageAll(peer, "thread/loaded/list", {});
|
|
66
|
+
threadId = loaded.find(id => typeof id === "string" && id !== "");
|
|
67
|
+
if (threadId === undefined) return { supported: false, serverVersion,
|
|
68
|
+
reasonCode: "feature_probe_failed" };
|
|
69
|
+
}
|
|
70
|
+
queueEntries(await peer.request("thread/queue/list", { threadId }));
|
|
65
71
|
} catch (error) {
|
|
66
|
-
|
|
72
|
+
return { supported: false, serverVersion, reasonCode: error?.code === "ETIMEDOUT"
|
|
73
|
+
? "probe_timeout" : "protocol_mismatch" };
|
|
67
74
|
}
|
|
68
75
|
return { supported: true, serverVersion, reasonCode: null, modes: [...QUEUE_MODES] };
|
|
69
76
|
}
|
|
@@ -71,25 +78,70 @@ export async function probeCodexQueue(peer, { threadId, minimum = MINIMUM_VERSIO
|
|
|
71
78
|
async function pageAll(peer, method, params) {
|
|
72
79
|
const items = [];
|
|
73
80
|
let cursor = null;
|
|
81
|
+
const cursors = new Set();
|
|
74
82
|
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
75
83
|
const response = await peer.request(method, cursor === null ? params : { ...params, cursor });
|
|
76
|
-
|
|
84
|
+
if (!Array.isArray(response?.data)) throw protocolError();
|
|
85
|
+
items.push(...response.data);
|
|
77
86
|
cursor = response?.nextCursor ?? null;
|
|
78
|
-
if (cursor === null)
|
|
87
|
+
if (cursor === null) return items;
|
|
88
|
+
if (typeof cursor !== "string" || cursor === "" || cursors.has(cursor)) throw protocolError();
|
|
89
|
+
cursors.add(cursor);
|
|
79
90
|
}
|
|
91
|
+
throw protocolError();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const protocolError = () => Object.assign(new Error("invalid queue protocol response"),
|
|
95
|
+
{ code: "EPROTOCOL" });
|
|
96
|
+
|
|
97
|
+
function queueEntries(response) {
|
|
98
|
+
if (!Array.isArray(response?.data) || response.data.some(item => typeof item?.id !== "string"
|
|
99
|
+
|| item.id === "" || (item.clientUserMessageId != null
|
|
100
|
+
&& typeof item.clientUserMessageId !== "string"))) throw protocolError();
|
|
101
|
+
return response.data;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function threadEntries(items) {
|
|
105
|
+
if (items.some(item => item === null || typeof item !== "object" || Array.isArray(item)
|
|
106
|
+
|| typeof item.id !== "string" || item.id === "")) throw protocolError();
|
|
80
107
|
return items;
|
|
81
108
|
}
|
|
82
109
|
|
|
83
|
-
|
|
110
|
+
export async function canonicalCwd(cwd) {
|
|
111
|
+
if (typeof cwd !== "string" || !path.isAbsolute(cwd) || cwd.includes("\0")) return null;
|
|
112
|
+
try {
|
|
113
|
+
const resolved = await realpath(cwd);
|
|
114
|
+
return (await stat(resolved)).isDirectory() ? resolved : null;
|
|
115
|
+
} catch { return null; }
|
|
116
|
+
}
|
|
84
117
|
|
|
85
118
|
export async function locateCodexThread(peer, { threadId, cwd }) {
|
|
86
119
|
const loaded = await pageAll(peer, "thread/loaded/list", {});
|
|
87
120
|
if (!loaded.includes(threadId)) return { found: false, reasonCode: "thread_not_loaded" };
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
// Filter locally after canonicalization: a server-side lexical cwd filter
|
|
122
|
+
// would hide a thread recorded through a symlink to the same directory.
|
|
123
|
+
const threads = threadEntries(await pageAll(peer, "thread/list",
|
|
124
|
+
{ limit: 100, useStateDbOnly: true }));
|
|
125
|
+
const matches = threads.filter(item => item.id === threadId);
|
|
126
|
+
let found = matches.length === 1 ? matches[0] : null;
|
|
127
|
+
if (matches.length === 0) {
|
|
128
|
+
// A real first SessionStart/UserPromptSubmit runs before persistence has
|
|
129
|
+
// materialized the thread. The loaded ID still has an authoritative live
|
|
130
|
+
// metadata snapshot; includeTurns:false never asks for conversation history.
|
|
131
|
+
const response = await peer.request("thread/read", { threadId, includeTurns: false });
|
|
132
|
+
const metadata = response?.thread;
|
|
133
|
+
if (metadata?.id === threadId && Array.isArray(metadata.turns) && metadata.turns.length === 0) {
|
|
134
|
+
found = metadata;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
90
137
|
if (!found) return { found: false, reasonCode: "thread_not_found" };
|
|
91
|
-
|
|
92
|
-
|
|
138
|
+
const actualCwd = await canonicalCwd(found.cwd);
|
|
139
|
+
if (actualCwd === null || (cwd !== undefined && actualCwd !== await canonicalCwd(cwd))) {
|
|
140
|
+
return { found: false, reasonCode: "cwd_mismatch" };
|
|
141
|
+
}
|
|
142
|
+
const status = found.status?.type;
|
|
143
|
+
if (!["idle", "active"].includes(status)) return { found: false, reasonCode: "thread_not_loaded" };
|
|
144
|
+
return { found: true, threadId, cwd: actualCwd, status };
|
|
93
145
|
}
|
|
94
146
|
|
|
95
147
|
// thread/queue/list first, so a retried client message id is the same offer
|
|
@@ -97,14 +149,15 @@ export async function locateCodexThread(peer, { threadId, cwd }) {
|
|
|
97
149
|
// clientUserMessageId.
|
|
98
150
|
export async function addCodexQueueMessage(peer, { threadId, messageId, text }) {
|
|
99
151
|
const listed = await peer.request("thread/queue/list", { threadId });
|
|
100
|
-
const existing = (listed
|
|
152
|
+
const existing = queueEntries(listed).find(item => item.clientUserMessageId === messageId);
|
|
101
153
|
if (existing) {
|
|
102
154
|
return { accepted: true, duplicate: true, queuedSubmissionId: existing.id };
|
|
103
155
|
}
|
|
104
156
|
const added = await peer.request("thread/queue/add", { threadId,
|
|
105
157
|
input: [{ type: "text", text }], clientUserMessageId: messageId });
|
|
106
158
|
const submission = added?.queuedSubmission;
|
|
107
|
-
if (!submission || submission.
|
|
159
|
+
if (!submission || typeof submission.id !== "string" || submission.id === ""
|
|
160
|
+
|| submission.clientUserMessageId !== messageId) {
|
|
108
161
|
throw Object.assign(new Error("queue acknowledgement did not echo the client message id"),
|
|
109
162
|
{ code: "EPROTOCOL" });
|
|
110
163
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { BEGIN, END, renderBlock } from "@agents-can-communicate/adapter-sdk";
|
|
4
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
5
|
+
|
|
6
|
+
const unsafe = (reason = "unrecognized TOML declaration") => {
|
|
7
|
+
throw new AccError(EXIT.CONFLICT, `cannot safely edit Codex config: ${reason}; `
|
|
8
|
+
+ "repair ambiguous TOML or ACC markers before retrying", { reason });
|
|
9
|
+
};
|
|
10
|
+
const ROOT = "# ACC sandbox writable_roots created with: ";
|
|
11
|
+
export const sandboxOwnership = root => `${ROOT}${JSON.stringify(root)}`;
|
|
12
|
+
|
|
13
|
+
const OWNED = new Map([
|
|
14
|
+
[JSON.stringify(["marketplaces", "acc-local"]), ["source_type", "source"]],
|
|
15
|
+
[JSON.stringify(["plugins", "agents-can-communicate@acc-local"]), ["enabled"]],
|
|
16
|
+
[JSON.stringify(["sandbox_workspace_write"]), ["writable_roots"]],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
// A bounded lexical scan, not a TOML value parser. Newlines inside strings,
|
|
20
|
+
// arrays and inline tables cannot introduce a header or a managed marker.
|
|
21
|
+
function* statements(source) {
|
|
22
|
+
let start = 0, code = "", quote = null;
|
|
23
|
+
const stack = [];
|
|
24
|
+
for (let i = 0; i < source.length;) {
|
|
25
|
+
const char = source[i];
|
|
26
|
+
if (quote) {
|
|
27
|
+
if (quote[0] === '"' && char === "\\") {
|
|
28
|
+
code += source.slice(i, i + 2); i += 2; continue;
|
|
29
|
+
}
|
|
30
|
+
if (source.startsWith(quote, i)) {
|
|
31
|
+
let count = quote.length;
|
|
32
|
+
if (quote.length === 3) {
|
|
33
|
+
while (source[i + count] === quote[0]) count++;
|
|
34
|
+
if (count > 5) unsafe("unsupported string quote delimiter");
|
|
35
|
+
}
|
|
36
|
+
code += source.slice(i, i + count); i += count; quote = null; continue;
|
|
37
|
+
}
|
|
38
|
+
if (quote.length === 1 && /[\r\n]/.test(char)) unsafe("newline in single-line string");
|
|
39
|
+
code += char; i++; continue;
|
|
40
|
+
}
|
|
41
|
+
if (char === "#") {
|
|
42
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === '"' || char === "'") {
|
|
46
|
+
quote = source.startsWith(char.repeat(3), i) ? char.repeat(3) : char;
|
|
47
|
+
code += quote; i += quote.length; continue;
|
|
48
|
+
}
|
|
49
|
+
if (char === "[" || char === "{") stack.push(char);
|
|
50
|
+
if (char === "]" || char === "}") {
|
|
51
|
+
if (stack.pop() !== (char === "]" ? "[" : "{")) unsafe("mismatched collection delimiter");
|
|
52
|
+
}
|
|
53
|
+
code += char; i++;
|
|
54
|
+
if (char === "\n" && stack.length === 0) {
|
|
55
|
+
yield { raw: source.slice(start, i), code: code.trim() };
|
|
56
|
+
start = i; code = "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (quote || stack.length) unsafe(quote ? "unclosed string" : "unclosed collection");
|
|
60
|
+
if (start < source.length) yield { raw: source.slice(start), code: code.trim() };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function decodeKey(key) {
|
|
64
|
+
if (key[0] === "'") return key.slice(1, -1);
|
|
65
|
+
if (key[0] !== '"') return key;
|
|
66
|
+
return key.slice(1, -1).replace(/\\(U[\da-fA-F]{8}|u[\da-fA-F]{4}|[btnfr"\\])|\\./g,
|
|
67
|
+
(match, escape) => {
|
|
68
|
+
if (!escape) unsafe("unsupported quoted-key escape");
|
|
69
|
+
if (/^[uU]/.test(escape)) {
|
|
70
|
+
const point = Number.parseInt(escape.slice(1), 16);
|
|
71
|
+
if (point > 0x10ffff || (point >= 0xd800 && point <= 0xdfff)) {
|
|
72
|
+
unsafe("invalid quoted-key Unicode escape");
|
|
73
|
+
}
|
|
74
|
+
return String.fromCodePoint(point);
|
|
75
|
+
}
|
|
76
|
+
return ({ b: "\b", t: "\t", n: "\n", f: "\f", r: "\r", '"': '"', "\\": "\\" })[escape];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function keyPath(code) {
|
|
81
|
+
const keys = [];
|
|
82
|
+
let rest = code.trimStart();
|
|
83
|
+
for (;;) {
|
|
84
|
+
const match = /^(?:[A-Za-z0-9_-]+|'[^'\r\n]*'|"(?:[^"\\\r\n]|\\[^\r\n])*")/.exec(rest);
|
|
85
|
+
if (!match) unsafe("unrecognized TOML key");
|
|
86
|
+
keys.push(decodeKey(match[0]));
|
|
87
|
+
rest = rest.slice(match[0].length).trimStart();
|
|
88
|
+
if (!rest.startsWith(".")) return { keys, rest };
|
|
89
|
+
rest = rest.slice(1).trimStart();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function declaration(code) {
|
|
94
|
+
const header = code.startsWith("[");
|
|
95
|
+
const array = header && code.startsWith("[[");
|
|
96
|
+
const { keys, rest } = keyPath(header ? code.slice(array ? 2 : 1) : code);
|
|
97
|
+
if (header ? rest !== (array ? "]]" : "]") : !rest.startsWith("=")) unsafe();
|
|
98
|
+
return { keys, header, array, value: header ? null : rest.slice(1).trim() };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// An unchanged, newly generated sandbox has value provenance. A legacy or
|
|
102
|
+
// edited sandbox belongs to the client as a whole and remains byte-preserved.
|
|
103
|
+
function ownsSandbox(body) {
|
|
104
|
+
const metadata = body.filter(item => !item.code && item.raw.trim().startsWith(ROOT));
|
|
105
|
+
const values = body.filter(item => item.code);
|
|
106
|
+
if (metadata.length !== 1 || values.length !== 1) return false;
|
|
107
|
+
const value = values[0].code.match(/^writable_roots\s*=\s*(\[.*\])$/);
|
|
108
|
+
if (!value) return false;
|
|
109
|
+
try {
|
|
110
|
+
const root = JSON.parse(metadata[0].raw.trim().slice(ROOT.length));
|
|
111
|
+
const roots = JSON.parse(value[1]);
|
|
112
|
+
return typeof root === "string" && Array.isArray(roots)
|
|
113
|
+
&& roots.length === 1 && roots[0] === root;
|
|
114
|
+
} catch { return false; }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function assertRegistration(body, keys) {
|
|
118
|
+
const values = body.filter(item => item.code).map(item => declaration(item.code));
|
|
119
|
+
if (values.some(item => item.keys.length !== 1 || !keys.includes(item.keys[0]))) {
|
|
120
|
+
unsafe("unknown key in owned table");
|
|
121
|
+
}
|
|
122
|
+
const valid = { source_type: /^["']local["']$/, source: /^(?:"(?:[^"\\]|\\.)*"|'[^']*')$/,
|
|
123
|
+
enabled: /^(?:true|false)$/ };
|
|
124
|
+
if (values.length !== keys.length || keys.some(key => values.filter(item =>
|
|
125
|
+
item.keys.length === 1 && item.keys[0] === key && valid[key].test(item.value)).length !== 1)) {
|
|
126
|
+
unsafe("changed or incomplete owned registration");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function tableBody(entries, index) {
|
|
131
|
+
const body = [];
|
|
132
|
+
for (const item of entries.slice(index + 1)) {
|
|
133
|
+
if (item.code && declaration(item.code).header) break;
|
|
134
|
+
body.push(item);
|
|
135
|
+
}
|
|
136
|
+
return body;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Ownership is an exact generated table inside real ACC comment markers, with
|
|
141
|
+
* only its generated keys. Other tables (including descendants) and comments
|
|
142
|
+
* are foreign. Unknown keys in an owned table are ambiguous: refuse, never
|
|
143
|
+
* discard them. Codex may insert foreign tables before END; retain their raw
|
|
144
|
+
* bytes in order and append the replacement block after them.
|
|
145
|
+
*/
|
|
146
|
+
function inspect(source) {
|
|
147
|
+
const kept = [], declarations = [];
|
|
148
|
+
let inside = false, owned = null, table = [];
|
|
149
|
+
const entries = [...statements(source)];
|
|
150
|
+
for (const [index, { raw, code }] of entries.entries()) {
|
|
151
|
+
if (!code && raw.trim() === BEGIN) {
|
|
152
|
+
if (inside) unsafe("nested BEGIN marker");
|
|
153
|
+
inside = true; continue;
|
|
154
|
+
}
|
|
155
|
+
if (!code && raw.trim() === END) {
|
|
156
|
+
if (!inside) unsafe("unmatched END marker");
|
|
157
|
+
inside = false; continue;
|
|
158
|
+
}
|
|
159
|
+
if (!code) {
|
|
160
|
+
if (!inside || !owned || (raw.trim() && !(table.length === 1
|
|
161
|
+
&& table[0] === "sandbox_workspace_write" && raw.trim().startsWith(ROOT)))) kept.push(raw);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const entry = declaration(code);
|
|
165
|
+
if (entry.header) {
|
|
166
|
+
table = entry.keys;
|
|
167
|
+
owned = inside ? OWNED.get(JSON.stringify(table)) : null;
|
|
168
|
+
if (owned && table.length === 1 && table[0] === "sandbox_workspace_write"
|
|
169
|
+
&& !ownsSandbox(tableBody(entries, index))) owned = null;
|
|
170
|
+
if (owned && entry.array) unsafe("owned table declared as an array");
|
|
171
|
+
if (owned && table[0] !== "sandbox_workspace_write") {
|
|
172
|
+
assertRegistration(tableBody(entries, index), owned);
|
|
173
|
+
}
|
|
174
|
+
} else if (owned) {
|
|
175
|
+
// Markers are comments, so END does not close a TOML table. Removing its
|
|
176
|
+
// header would attach any following foreign assignment to another table.
|
|
177
|
+
if (!inside) unsafe("assignment continues owned table after END marker");
|
|
178
|
+
if (entry.keys.length !== 1 || !owned.includes(entry.keys[0])) {
|
|
179
|
+
unsafe("unknown key in owned table");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!owned) {
|
|
183
|
+
kept.push(raw);
|
|
184
|
+
declarations.push({ keys: entry.header ? table : [...table, ...entry.keys],
|
|
185
|
+
header: entry.header });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (inside) unsafe("missing END marker");
|
|
189
|
+
const has = prefix => declarations.some(({ keys }) => prefix.every((key, i) => keys[i] === key));
|
|
190
|
+
// Assignments occupy their exact namespace: even an empty inline parent is
|
|
191
|
+
// closed to later child tables. Ordinary headers and sibling keys stay valid.
|
|
192
|
+
const assignedParent = declarations.some(({ keys, header }) => !header
|
|
193
|
+
&& keys.length === 1 && ["marketplaces", "plugins"].includes(keys[0]));
|
|
194
|
+
return { source: kept.join(""),
|
|
195
|
+
sandbox: has(["sandbox_workspace_write"]),
|
|
196
|
+
registration: assignedParent || has(["marketplaces", "acc-local"])
|
|
197
|
+
|| has(["plugins", "agents-can-communicate@acc-local"]) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function inspectConfig(source, file = "config.toml") {
|
|
201
|
+
try {
|
|
202
|
+
return inspect(source);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (!(error instanceof AccError)) throw error;
|
|
205
|
+
// Structural reasons are fixed labels: never include key names or values.
|
|
206
|
+
throw new AccError(error.code, `${file}: ${error.message}`, { ...error.details, file });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function readConfig(file) {
|
|
211
|
+
return readFile(file, "utf8").catch(error => {
|
|
212
|
+
if (error.code === "ENOENT") return "";
|
|
213
|
+
throw error;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function writeTomlBlock(file, body) {
|
|
218
|
+
const { source } = inspectConfig(await readConfig(file), file);
|
|
219
|
+
const separator = source === "" || source.endsWith("\n") ? "" : "\n";
|
|
220
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
221
|
+
await writeFile(file, `${source}${separator}${renderBlock(body)}\n`);
|
|
222
|
+
return file;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function removeTomlBlock(file) {
|
|
226
|
+
const before = await readConfig(file);
|
|
227
|
+
const { source } = inspectConfig(before, file);
|
|
228
|
+
if (source === before) return false;
|
|
229
|
+
await writeFile(file, source);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
@@ -148,5 +148,5 @@ export function allowOutcome() {
|
|
|
148
148
|
* as data, because at this role the model reads text as instruction.
|
|
149
149
|
*/
|
|
150
150
|
export function injectOutcome(context) {
|
|
151
|
-
return { stdout: context
|
|
151
|
+
return { stdout: context, stderr: "", exitCode: 0 };
|
|
152
152
|
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
// Installation, removal, detection and planning share the exact marketplace,
|
|
2
|
+
// cache and config paths below; keeping them together avoids divergent ownership.
|
|
1
3
|
import { cp, mkdir, readdir, readFile, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
|
|
5
7
|
import { bakeSkillCommand, blankJson, blankText, removeIfEmpty, removeInstalledTree,
|
|
6
8
|
keepOnlyVersion, ownVersion, stampPluginVersion,
|
|
7
|
-
|
|
8
|
-
writeForeignJson, writeHookShim, writeTomlBlock }
|
|
9
|
+
tomlString, writeForeignJson, writeHookShim }
|
|
9
10
|
from "@agents-can-communicate/adapter-sdk";
|
|
10
11
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
12
|
+
import { inspectConfig, readConfig, removeTomlBlock, sandboxOwnership, writeTomlBlock } from "./config-block.mjs";
|
|
13
|
+
|
|
14
|
+
// Kept cohesive above 300 lines because Codex plugin install, cache, config,
|
|
15
|
+
// detection, and ownership share one client topology; splitting would duplicate
|
|
16
|
+
// path authority and make install/uninstall symmetry harder to audit.
|
|
17
|
+
|
|
11
18
|
|
|
12
19
|
const bundle = fileURLToPath(new URL("../plugin", import.meta.url));
|
|
13
20
|
const PLUGIN_NAME = "agents-can-communicate";
|
|
@@ -24,6 +31,8 @@ const PLUGIN_NAME = "agents-can-communicate";
|
|
|
24
31
|
// again to confirm a root of ACC's own is accepted and reported enabled.
|
|
25
32
|
const MARKETPLACE = "acc-local";
|
|
26
33
|
const QUALIFIED = `${PLUGIN_NAME}@${MARKETPLACE}`;
|
|
34
|
+
const HOOK_REVIEW = "hook readiness unverified: open codex; check ACC is enabled in /plugins; in /hooks, review "
|
|
35
|
+
+ "each ACC hook and enable/trust its current definition if needed, then restart the session";
|
|
27
36
|
|
|
28
37
|
// A marketplace is a root holding `.agents/plugins/marketplace.json`, and every
|
|
29
38
|
// `source.path` in that manifest - `./plugins/<name>` - is resolved by this
|
|
@@ -79,11 +88,12 @@ async function readJson(file, fallback) {
|
|
|
79
88
|
// Replace the bundle's placeholder command with the shim just written. The
|
|
80
89
|
// client copies an installed plugin into a cache of its own, so the command has
|
|
81
90
|
// to be absolute: a path relative to the bundle would not survive the copy.
|
|
91
|
+
const shellLiteral = value => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
82
92
|
const withShim = (wiring, shim) => ({ ...wiring, hooks: Object.fromEntries(
|
|
83
93
|
Object.entries(wiring.hooks).map(([event, entries]) => [event, entries.map(entry => ({
|
|
84
94
|
...entry,
|
|
85
95
|
hooks: entry.hooks.map(hook => ({ ...hook,
|
|
86
|
-
command: `sh
|
|
96
|
+
command: `sh ${shellLiteral(shim)} ${hook.command.split(" ").pop()}` })),
|
|
87
97
|
}))])) });
|
|
88
98
|
|
|
89
99
|
const writeJson = async (file, value) => {
|
|
@@ -136,20 +146,20 @@ const entryFor = () => ({
|
|
|
136
146
|
const sandboxTable = (stateRoot, theirs) => {
|
|
137
147
|
if (typeof stateRoot !== "string" || stateRoot === "") return [];
|
|
138
148
|
if (theirs) return [];
|
|
139
|
-
return ["", "[sandbox_workspace_write]",
|
|
149
|
+
return ["", "[sandbox_workspace_write]", sandboxOwnership(stateRoot),
|
|
140
150
|
`writable_roots = [${tomlString(stateRoot)}]`];
|
|
141
151
|
};
|
|
142
152
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
const declaresSandbox = config => inspectConfig(config).sandbox;
|
|
154
|
+
|
|
155
|
+
const sandboxReview = (config, file, stateRoot) =>
|
|
156
|
+
typeof stateRoot === "string" && stateRoot !== "" && inspectConfig(config, file).sandbox
|
|
157
|
+
? [`verify sandbox_workspace_write.writable_roots in ${file} includes ${stateRoot}; `
|
|
158
|
+
+ "your existing sandbox configuration was preserved"]
|
|
159
|
+
: [];
|
|
150
160
|
|
|
151
161
|
export async function installCodexPlugin({ home, agentsHome = home,
|
|
152
|
-
codexHome = path.join(home, ".codex"), stateRoot, runner, node }) {
|
|
162
|
+
codexHome = path.join(home, ".codex"), dataHome, stateRoot, runner, node, cli, preserveVersions = false }) {
|
|
153
163
|
// Read before writing, so a manifest that will not parse is found before a
|
|
154
164
|
// plugin tree is laid down that nothing will then be able to remove.
|
|
155
165
|
const existing = await readJson(marketplacePath(agentsHome), { name: MARKETPLACE,
|
|
@@ -159,15 +169,26 @@ export async function installCodexPlugin({ home, agentsHome = home,
|
|
|
159
169
|
// marketplace at this root - discovered without any config entry, under
|
|
160
170
|
// whatever its manifest calls itself - the id ACC enabled was one the client
|
|
161
171
|
// never forms, and the plugin sat there listed and not installed.
|
|
162
|
-
const
|
|
172
|
+
const config = configPath(codexHome);
|
|
173
|
+
const before = await readConfig(config);
|
|
174
|
+
const foreign = inspectConfig(before, config);
|
|
175
|
+
// Preflight before touching installed files: a duplicate table makes Codex
|
|
176
|
+
// refuse the config, and ambiguous ownership must never delete client state.
|
|
177
|
+
if (foreign.registration) {
|
|
178
|
+
throw new AccError(EXIT.CONFLICT,
|
|
179
|
+
`marketplace ${MARKETPLACE} or its plugin is already registered in this config; `
|
|
180
|
+
+ "remove it and install again", { config });
|
|
181
|
+
}
|
|
182
|
+
const theirSandbox = foreign.sandbox;
|
|
163
183
|
|
|
164
184
|
const target = pluginPath(agentsHome);
|
|
165
185
|
await rm(target, { recursive: true, force: true });
|
|
166
186
|
await cp(bundle, target, { recursive: true });
|
|
167
187
|
// The skill ships with a placeholder where the command belongs: `acc` is
|
|
168
188
|
// not on PATH everywhere, and an agent that cannot run it improvises.
|
|
169
|
-
await bakeSkillCommand({ root: target, node });
|
|
170
|
-
const shim = await writeHookShim({ dir: target, adapterId: "codex",
|
|
189
|
+
await bakeSkillCommand({ root: target, node, cli, dataHome });
|
|
190
|
+
const shim = await writeHookShim({ dir: target, adapterId: "codex",
|
|
191
|
+
dataHome, runner, node });
|
|
171
192
|
await writeJson(path.join(target, "hooks.json"),
|
|
172
193
|
withShim(await readJson(path.join(bundle, "hooks.json"), { hooks: {} }), shim));
|
|
173
194
|
|
|
@@ -178,16 +199,6 @@ export async function installCodexPlugin({ home, agentsHome = home,
|
|
|
178
199
|
const others = (existing.plugins ?? []).filter(entry => entry.name !== PLUGIN_NAME);
|
|
179
200
|
await writeMarketplace(file, { ...existing, plugins: [...others, entryFor()] });
|
|
180
201
|
|
|
181
|
-
const config = configPath(codexHome);
|
|
182
|
-
// A marketplace declared twice makes this client refuse the whole config, and
|
|
183
|
-
// then every plugin the user has stops working. If they registered it
|
|
184
|
-
// themselves, say so rather than appending a duplicate table.
|
|
185
|
-
if (stripBlock(before).includes(`[marketplaces.${MARKETPLACE}]`)) {
|
|
186
|
-
throw new AccError(EXIT.CONFLICT,
|
|
187
|
-
`marketplace ${MARKETPLACE} is already registered in this config; `
|
|
188
|
-
+ "remove it and install again", { config });
|
|
189
|
-
}
|
|
190
|
-
const theirSandbox = declaresSandbox(stripBlock(before));
|
|
191
202
|
await writeTomlBlock(config, [
|
|
192
203
|
`[marketplaces.${MARKETPLACE}]`,
|
|
193
204
|
`source_type = "local"`,
|
|
@@ -212,7 +223,7 @@ export async function installCodexPlugin({ home, agentsHome = home,
|
|
|
212
223
|
await cp(target, cached, { recursive: true });
|
|
213
224
|
// One copy, the one just written - and only inside this plugin's own
|
|
214
225
|
// directory. The marketplace cache root above it holds other people's plugins.
|
|
215
|
-
await keepOnlyVersion({ root: path.dirname(cached), version, io: { readdir, rm } });
|
|
226
|
+
if (!preserveVersions) await keepOnlyVersion({ root: path.dirname(cached), version, io: { readdir, rm } });
|
|
216
227
|
|
|
217
228
|
// The plugin's own directory in the cache. Not the versioned one inside it,
|
|
218
229
|
// which goes stale the moment the version changes - and not the marketplace
|
|
@@ -224,11 +235,9 @@ export async function installCodexPlugin({ home, agentsHome = home,
|
|
|
224
235
|
// while ACC invented its own marketplace name and so had a root to itself.
|
|
225
236
|
// make the record stale the moment the plugin version changes.
|
|
226
237
|
return { ok: true, changes: [target, file, config, cachePath(codexHome)],
|
|
238
|
+
needsAction: [HOOK_REVIEW, ...sandboxReview(before, config, stateRoot)],
|
|
227
239
|
diagnostics: ["hooks require explicit trust in Codex before they run",
|
|
228
|
-
...(
|
|
229
|
-
? [`this config sets its own sandbox_workspace_write; add ${stateRoot} to `
|
|
230
|
-
+ "writable_roots, or an agent here can read the roster and record nothing"]
|
|
231
|
-
: [])] };
|
|
240
|
+
...sandboxReview(before, config, stateRoot)] };
|
|
232
241
|
}
|
|
233
242
|
|
|
234
243
|
/** Remove each directory that is empty, in the order given. */
|
|
@@ -239,10 +248,21 @@ async function removeEmptyDirs(directories) {
|
|
|
239
248
|
}
|
|
240
249
|
|
|
241
250
|
|
|
251
|
+
export async function preflightCodexUninstall({ home, agentsHome = home,
|
|
252
|
+
codexHome = path.join(home, ".codex") }) {
|
|
253
|
+
const existing = await readJson(marketplacePath(agentsHome), null);
|
|
254
|
+
const before = await readFile(configPath(codexHome), "utf8").catch(error => {
|
|
255
|
+
if (error.code === "ENOENT") return "";
|
|
256
|
+
throw error;
|
|
257
|
+
});
|
|
258
|
+
return { existing, withoutOurs: inspectConfig(before, configPath(codexHome)).source };
|
|
259
|
+
}
|
|
260
|
+
|
|
242
261
|
export async function uninstallCodexPlugin({ home, agentsHome = home,
|
|
243
262
|
codexHome = path.join(home, ".codex"), keep = [] }) {
|
|
244
263
|
const file = marketplacePath(agentsHome);
|
|
245
|
-
|
|
264
|
+
// Direct adapter callers need the same check as the installer boundary.
|
|
265
|
+
const { existing, withoutOurs } = await preflightCodexUninstall({ home, agentsHome, codexHome });
|
|
246
266
|
const changes = [];
|
|
247
267
|
if (existing !== null) {
|
|
248
268
|
const kept = (existing.plugins ?? []).filter(entry => entry.name !== PLUGIN_NAME);
|
|
@@ -259,9 +279,9 @@ export async function uninstallCodexPlugin({ home, agentsHome = home,
|
|
|
259
279
|
// `hook: SessionStart Completed` while executing nothing, and ACC's write
|
|
260
280
|
// guard is silently off while `acc doctor` reports the adapter installed.
|
|
261
281
|
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
282
|
+
// Those historical hashes admitted the historical hooks. Current Codex checks
|
|
283
|
+
// exact definitions, so preservation cannot establish present readiness. The
|
|
284
|
+
// decision remains the client's to keep or revise, never ACC's to remove.
|
|
265
285
|
// The marketplace directory is ACC's too, so it goes rather than being left
|
|
266
286
|
// behind empty.
|
|
267
287
|
// A blank TOML config and an absent one are the same to this client, and a
|
|
@@ -292,54 +312,42 @@ export async function uninstallCodexPlugin({ home, agentsHome = home,
|
|
|
292
312
|
marketplaceRoot(agentsHome),
|
|
293
313
|
cacheRoot(codexHome),
|
|
294
314
|
]);
|
|
295
|
-
return { ok: true, changes, diagnostics:
|
|
315
|
+
return { ok: true, changes, diagnostics: declaresSandbox(withoutOurs)
|
|
316
|
+
? ["existing sandbox_workspace_write configuration was preserved; review any retained writable_roots after removal"]
|
|
317
|
+
: [] };
|
|
296
318
|
}
|
|
297
319
|
|
|
298
320
|
export async function detectCodex({ home, agentsHome = home,
|
|
299
|
-
codexHome = path.join(home, ".codex") }) {
|
|
321
|
+
codexHome = path.join(home, ".codex"), stateRoot }) {
|
|
300
322
|
const marketplace = await readJson(marketplacePath(agentsHome), null);
|
|
301
323
|
const published = (marketplace?.plugins ?? []).some(entry => entry.name === PLUGIN_NAME);
|
|
302
324
|
const config = await readFile(configPath(codexHome), "utf8").catch(() => "");
|
|
303
325
|
const registered = config.includes(`[marketplaces.${MARKETPLACE}]`);
|
|
304
|
-
const
|
|
326
|
+
const pluginEntry = config.includes(`[plugins."${QUALIFIED}"]`);
|
|
305
327
|
const cached = await stat(cachePath(codexHome))
|
|
306
328
|
.then(() => true).catch(() => false);
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
// is off. Only asked when something is wired: a warning about hooks that do
|
|
312
|
-
// not exist is how a real one gets ignored.
|
|
313
|
-
const untrusted = cached && !config.includes(`hooks.state."${QUALIFIED}:`);
|
|
329
|
+
// Saved trust is not readiness. Codex compares every current definition's
|
|
330
|
+
// hash and can disable a trusted hook. Even a commented or stale single record
|
|
331
|
+
// previously suppressed this check. Leave verification to Codex's /hooks;
|
|
332
|
+
// detection neither invents its hash algorithm nor starts a client service.
|
|
314
333
|
return { ok: true, changes: [], diagnostics: [
|
|
315
334
|
published ? "acc plugin published in the marketplace" : "acc plugin not registered",
|
|
316
|
-
registered &&
|
|
317
|
-
? "marketplace
|
|
318
|
-
: "marketplace not
|
|
335
|
+
registered && pluginEntry
|
|
336
|
+
? "marketplace and plugin entries found in config; activation not verified"
|
|
337
|
+
: "marketplace/plugin registration not verified from this config",
|
|
319
338
|
// Publishing, registering and enabling are all necessary and still not
|
|
320
339
|
// sufficient: hooks stay silent until the client copies the plugin into its
|
|
321
340
|
// own cache. Only the client does that, so ACC names the command.
|
|
322
341
|
cached
|
|
323
342
|
? "plugin installed in the client's cache"
|
|
324
343
|
: `plugin not installed yet; run: codex plugin add ${QUALIFIED}`,
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
// else - published, registered, enabled, cached - can be true while the
|
|
329
|
-
// write guard is off. Said only when there is something to trust, because a
|
|
330
|
-
// warning on a machine with nothing wired is how a real one gets ignored.
|
|
331
|
-
...(untrusted
|
|
332
|
-
? ["hooks are not trusted, so this client reports them completed and runs "
|
|
333
|
-
+ "nothing"]
|
|
344
|
+
...(cached
|
|
345
|
+
? ["hook readiness is unverified by ACC; Codex checks whether each current "
|
|
346
|
+
+ "definition is enabled and trusted"]
|
|
334
347
|
: []),
|
|
335
348
|
],
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// A diagnostic alone would have stayed in `--json`, which is where the first
|
|
339
|
-
// version of this fix put it and where nobody would have read it.
|
|
340
|
-
needsAction: untrusted
|
|
341
|
-
? ["start codex once and accept the hook trust prompt "
|
|
342
|
-
+ "# its hooks run nothing until then"]
|
|
349
|
+
needsAction: cached
|
|
350
|
+
? [HOOK_REVIEW, ...sandboxReview(config, configPath(codexHome), stateRoot)]
|
|
343
351
|
: [] };
|
|
344
352
|
}
|
|
345
353
|
|