@jskit-ai/assistant-core 0.1.200 → 0.1.202
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/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
createCodexAppServerIsolation,
|
|
5
|
+
codexToolFreeConfiguration,
|
|
6
|
+
readCodexToolFreeConfiguration
|
|
7
|
+
} from "../src/server/conversation/codexConfiguration.js";
|
|
8
|
+
|
|
9
|
+
// Carried from the original public helper bridge tests. Public profile, schema,
|
|
10
|
+
// prompt and request-settings assertions remain in that unchanged test file.
|
|
11
|
+
const MINIMUM_CODEX_VERSION = "0.151.0";
|
|
12
|
+
const POLICY_UNENFORCEABLE = "codex_policy_unenforceable";
|
|
13
|
+
const HELPER_DISABLED_FEATURES = Object.freeze([
|
|
14
|
+
"apps",
|
|
15
|
+
"artifact",
|
|
16
|
+
"browser_use",
|
|
17
|
+
"browser_use_external",
|
|
18
|
+
"browser_use_full_cdp_access",
|
|
19
|
+
"code_mode",
|
|
20
|
+
"code_mode_host",
|
|
21
|
+
"computer_use",
|
|
22
|
+
"current_time_reminder",
|
|
23
|
+
"default_mode_request_user_input",
|
|
24
|
+
"deferred_executor",
|
|
25
|
+
"goals",
|
|
26
|
+
"hooks",
|
|
27
|
+
"image_generation",
|
|
28
|
+
"in_app_browser",
|
|
29
|
+
"memories",
|
|
30
|
+
"multi_agent",
|
|
31
|
+
"multi_agent_v2",
|
|
32
|
+
"plugins",
|
|
33
|
+
"psp",
|
|
34
|
+
"recommended_plugins",
|
|
35
|
+
"request_permissions_tool",
|
|
36
|
+
"shell_tool",
|
|
37
|
+
"skill_mcp_dependency_install",
|
|
38
|
+
"skill_search",
|
|
39
|
+
"sleep_tool",
|
|
40
|
+
"tool_call_mcp_elicitation",
|
|
41
|
+
"tool_suggest",
|
|
42
|
+
"token_budget",
|
|
43
|
+
"unified_exec",
|
|
44
|
+
"unified_exec_zsh_fork",
|
|
45
|
+
"view_image"
|
|
46
|
+
]);
|
|
47
|
+
const isolation = createCodexAppServerIsolation({
|
|
48
|
+
clientName: "vibe64",
|
|
49
|
+
minimumVersion: MINIMUM_CODEX_VERSION,
|
|
50
|
+
disabledFeatures: HELPER_DISABLED_FEATURES
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const HELPER_EXECUTION_CWD = "/runtime/vibe64/codex-helper/workspace";
|
|
54
|
+
const HELPER_ACCOUNT_IDENTITY_SIGNATURE = `sha256:${"a".repeat(64)}`;
|
|
55
|
+
|
|
56
|
+
function helperExecutionProviderParts() {
|
|
57
|
+
return {
|
|
58
|
+
async currentHelperExecutionContext() {
|
|
59
|
+
return {
|
|
60
|
+
accountIdentitySignature: HELPER_ACCOUNT_IDENTITY_SIGNATURE,
|
|
61
|
+
cwd: HELPER_EXECUTION_CWD,
|
|
62
|
+
executionMode: "helper"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function helperInventoryProvider({
|
|
69
|
+
generation = 1,
|
|
70
|
+
hooks = [],
|
|
71
|
+
mcpServers = {},
|
|
72
|
+
records = null,
|
|
73
|
+
userAgent = `vibe64/${MINIMUM_CODEX_VERSION} (unit test)`
|
|
74
|
+
} = {}) {
|
|
75
|
+
const calls = [];
|
|
76
|
+
return {
|
|
77
|
+
...helperExecutionProviderParts(),
|
|
78
|
+
calls,
|
|
79
|
+
currentConnectionGeneration() {
|
|
80
|
+
return generation;
|
|
81
|
+
},
|
|
82
|
+
currentServerInfo() {
|
|
83
|
+
return { userAgent };
|
|
84
|
+
},
|
|
85
|
+
async listHooks(cwds) {
|
|
86
|
+
calls.push(["listHooks", cwds]);
|
|
87
|
+
return {
|
|
88
|
+
data: records || [{
|
|
89
|
+
cwd: cwds[0],
|
|
90
|
+
errors: [],
|
|
91
|
+
hooks,
|
|
92
|
+
warnings: []
|
|
93
|
+
}]
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
async readConfig(params) {
|
|
97
|
+
calls.push(["readConfig", params]);
|
|
98
|
+
return {
|
|
99
|
+
config: {
|
|
100
|
+
mcp_servers: mcpServers
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The original application's prepare callback now supplies already authorized
|
|
108
|
+
// policy values. These fixtures exercise only the native ownership boundary.
|
|
109
|
+
async function prepareHelper({ provider }) {
|
|
110
|
+
const enforcement = await isolation.inspect(provider, { effort: "low", summary: "none" });
|
|
111
|
+
return { enforcement, settings: { config: enforcement.config, cwd: enforcement.executionCwd } };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function startHelper(options) {
|
|
115
|
+
return isolation.start(options.provider, () => prepareHelper(options));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resumeHelper(options) {
|
|
119
|
+
return isolation.resume(options.provider, options.threadId, () => prepareHelper(options));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test("Codex helper inventories preserve immutable native isolation and read ordering", async () => {
|
|
123
|
+
const provider = helperInventoryProvider({
|
|
124
|
+
hooks: [{
|
|
125
|
+
currentHash: "sha256:malicious-hook",
|
|
126
|
+
enabled: true,
|
|
127
|
+
handlerType: "command",
|
|
128
|
+
isManaged: false,
|
|
129
|
+
key: "/repo/worktree/.codex/hooks.json:stop:0:0",
|
|
130
|
+
sourcePath: "/repo/worktree/.codex/hooks.json"
|
|
131
|
+
}, {
|
|
132
|
+
currentHash: "sha256:disabled-managed-hook",
|
|
133
|
+
enabled: false,
|
|
134
|
+
handlerType: "command",
|
|
135
|
+
isManaged: true,
|
|
136
|
+
key: "managed:disabled:0",
|
|
137
|
+
sourcePath: "/etc/codex/hooks.json"
|
|
138
|
+
}],
|
|
139
|
+
mcpServers: {
|
|
140
|
+
"danger.server": {
|
|
141
|
+
command: "write-anywhere"
|
|
142
|
+
},
|
|
143
|
+
"quoted\"server": {
|
|
144
|
+
command: "exfiltrate"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const prepared = await prepareHelper({ provider });
|
|
149
|
+
|
|
150
|
+
assert.equal(prepared.enforcement.connectionGeneration, 1);
|
|
151
|
+
assert.deepEqual(prepared.enforcement.mcpServerNames, [
|
|
152
|
+
"danger.server",
|
|
153
|
+
"quoted\"server"
|
|
154
|
+
]);
|
|
155
|
+
assert.deepEqual(prepared.settings.config.mcp_servers, {
|
|
156
|
+
"danger.server": {
|
|
157
|
+
enabled: false
|
|
158
|
+
},
|
|
159
|
+
"quoted\"server": {
|
|
160
|
+
enabled: false
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
assert.deepEqual(prepared.settings.config.hooks, {
|
|
164
|
+
state: {
|
|
165
|
+
"/repo/worktree/.codex/hooks.json:stop:0:0": {
|
|
166
|
+
enabled: false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
assert.equal(prepared.settings.config.features.shell_tool, false);
|
|
171
|
+
assert.equal(prepared.settings.config.features.plugins, false);
|
|
172
|
+
assert.equal(prepared.settings.config.features.apps, false);
|
|
173
|
+
assert.equal(prepared.settings.config.features.browser_use, false);
|
|
174
|
+
assert.equal(prepared.settings.config.features.computer_use, false);
|
|
175
|
+
assert.equal(prepared.settings.config.features.hooks, false);
|
|
176
|
+
assert.equal(prepared.settings.config.features.multi_agent, false);
|
|
177
|
+
assert.equal(prepared.settings.config.features.tool_suggest, false);
|
|
178
|
+
assert.equal(prepared.settings.config.features.view_image, false);
|
|
179
|
+
assert.equal(prepared.settings.config.features.current_time_reminder, false);
|
|
180
|
+
assert.equal(prepared.settings.config.features.sleep_tool, false);
|
|
181
|
+
assert.equal(prepared.settings.config.features.token_budget, false);
|
|
182
|
+
assert.deepEqual(prepared.settings.config.notify, []);
|
|
183
|
+
assert.equal(prepared.settings.config.orchestrator.mcp.enabled, false);
|
|
184
|
+
assert.equal(prepared.settings.config.orchestrator.skills.enabled, false);
|
|
185
|
+
assert.equal(prepared.settings.config.skills.include_instructions, false);
|
|
186
|
+
assert.equal(prepared.settings.config.include_apps_instructions, false);
|
|
187
|
+
assert.equal(prepared.settings.config.include_permissions_instructions, false);
|
|
188
|
+
assert.equal(prepared.settings.config.include_environment_context, false);
|
|
189
|
+
assert.equal(prepared.settings.config.project_doc_max_bytes, 0);
|
|
190
|
+
assert.equal(Object.isFrozen(prepared.settings.config), true);
|
|
191
|
+
assert.equal(Object.isFrozen(prepared.settings.config.mcp_servers["danger.server"]), true);
|
|
192
|
+
|
|
193
|
+
assert.deepEqual(provider.calls, [[
|
|
194
|
+
"readConfig",
|
|
195
|
+
{
|
|
196
|
+
cwd: HELPER_EXECUTION_CWD,
|
|
197
|
+
includeLayers: false
|
|
198
|
+
}
|
|
199
|
+
], [
|
|
200
|
+
"listHooks",
|
|
201
|
+
[HELPER_EXECUTION_CWD]
|
|
202
|
+
]]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("Codex helper accepts the minimum and newer patch, minor and major versions with isolation", async () => {
|
|
206
|
+
const [major, minor, patch] = MINIMUM_CODEX_VERSION.split(".").map(Number);
|
|
207
|
+
for (const version of [
|
|
208
|
+
MINIMUM_CODEX_VERSION,
|
|
209
|
+
`${major}.${minor}.${patch + 1}`,
|
|
210
|
+
`${major}.${minor}.${patch + 10}`,
|
|
211
|
+
`${major}.${minor + 1}.0`,
|
|
212
|
+
`${major}.${minor + 10}.0`,
|
|
213
|
+
`${major + 1}.0.0`
|
|
214
|
+
]) {
|
|
215
|
+
const provider = helperInventoryProvider({ userAgent: `vibe64/${version} (unit test)` });
|
|
216
|
+
assert.deepEqual(isolation.assertCompatibility(provider), {
|
|
217
|
+
minimumVersion: MINIMUM_CODEX_VERSION,
|
|
218
|
+
version
|
|
219
|
+
});
|
|
220
|
+
const prepared = await prepareHelper({
|
|
221
|
+
provider
|
|
222
|
+
});
|
|
223
|
+
assert.equal(prepared.settings.config.features.shell_tool, false, version);
|
|
224
|
+
assert.equal(prepared.settings.config.features.hooks, false, version);
|
|
225
|
+
assert.equal(provider.calls.length, 2, version);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("Codex helper rejects versions below the minimum before inventory", async () => {
|
|
230
|
+
const minimumParts = MINIMUM_CODEX_VERSION.split(".").map(Number);
|
|
231
|
+
for (const [index, part] of minimumParts.entries()) {
|
|
232
|
+
if (part === 0) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const olderParts = [...minimumParts];
|
|
236
|
+
olderParts[index] -= 1;
|
|
237
|
+
olderParts.fill(999, index + 1);
|
|
238
|
+
const version = olderParts.join(".");
|
|
239
|
+
const provider = helperInventoryProvider({ userAgent: `vibe64/${version} (unit test)` });
|
|
240
|
+
await assert.rejects(prepareHelper({
|
|
241
|
+
provider
|
|
242
|
+
}), (error) => {
|
|
243
|
+
assert.equal(error.code, POLICY_UNENFORCEABLE);
|
|
244
|
+
assert.equal(error.minimumVersion, MINIMUM_CODEX_VERSION);
|
|
245
|
+
assert.equal(error.actualVersion, version);
|
|
246
|
+
assert.equal(error.message, `Codex helper execution requires app-server ${MINIMUM_CODEX_VERSION} or newer; current version is ${version}. Update Codex and retry.`);
|
|
247
|
+
return true;
|
|
248
|
+
});
|
|
249
|
+
assert.deepEqual(provider.calls, [], version);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("Codex helper fails closed before inventory when app-server cannot enforce the policy", async () => {
|
|
254
|
+
for (const [label, provider] of [
|
|
255
|
+
["missing version API", {
|
|
256
|
+
...helperInventoryProvider(),
|
|
257
|
+
currentServerInfo: undefined
|
|
258
|
+
}],
|
|
259
|
+
["malformed version", helperInventoryProvider({
|
|
260
|
+
userAgent: "vibe64/development"
|
|
261
|
+
})],
|
|
262
|
+
["spoofed product", helperInventoryProvider({
|
|
263
|
+
userAgent: `attacker/999.0.0 vibe64/${MINIMUM_CODEX_VERSION}`
|
|
264
|
+
})],
|
|
265
|
+
["leading zero", helperInventoryProvider({
|
|
266
|
+
userAgent: `vibe64/0${MINIMUM_CODEX_VERSION}`
|
|
267
|
+
})],
|
|
268
|
+
["control character", helperInventoryProvider({
|
|
269
|
+
userAgent: `vibe64/${MINIMUM_CODEX_VERSION}\nattacker/999.0.0`
|
|
270
|
+
})],
|
|
271
|
+
["oversized user agent", helperInventoryProvider({
|
|
272
|
+
userAgent: `vibe64/${MINIMUM_CODEX_VERSION} ${"x".repeat(600)}`
|
|
273
|
+
})],
|
|
274
|
+
["prerelease version", helperInventoryProvider({
|
|
275
|
+
userAgent: `vibe64/${MINIMUM_CODEX_VERSION}-beta.1 (unit test)`
|
|
276
|
+
})],
|
|
277
|
+
["unsafe version component", helperInventoryProvider({
|
|
278
|
+
userAgent: `vibe64/${Number.MAX_SAFE_INTEGER + 1}.0.0 (unit test)`
|
|
279
|
+
})]
|
|
280
|
+
]) {
|
|
281
|
+
await assert.rejects(prepareHelper({
|
|
282
|
+
provider
|
|
283
|
+
}), (error) => {
|
|
284
|
+
assert.equal(
|
|
285
|
+
error.code,
|
|
286
|
+
POLICY_UNENFORCEABLE,
|
|
287
|
+
label
|
|
288
|
+
);
|
|
289
|
+
assert.equal(error.minimumVersion, MINIMUM_CODEX_VERSION, label);
|
|
290
|
+
assert.match(error.message, /Update Codex and retry/u, label);
|
|
291
|
+
return true;
|
|
292
|
+
});
|
|
293
|
+
assert.deepEqual(provider.calls, [], label);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const supported = helperInventoryProvider();
|
|
297
|
+
await prepareHelper({
|
|
298
|
+
provider: supported
|
|
299
|
+
});
|
|
300
|
+
assert.equal(supported.calls.length, 2);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("Codex helper fails closed for managed hooks and incomplete hook discovery", async () => {
|
|
304
|
+
await assert.rejects(prepareHelper({
|
|
305
|
+
provider: helperInventoryProvider({
|
|
306
|
+
hooks: [{
|
|
307
|
+
currentHash: "sha256:managed-hook",
|
|
308
|
+
enabled: true,
|
|
309
|
+
handlerType: "command",
|
|
310
|
+
isManaged: true,
|
|
311
|
+
key: "managed:stop:0",
|
|
312
|
+
sourcePath: "/etc/codex/hooks.json"
|
|
313
|
+
}]
|
|
314
|
+
})
|
|
315
|
+
}), (error) => {
|
|
316
|
+
assert.equal(
|
|
317
|
+
error.code,
|
|
318
|
+
POLICY_UNENFORCEABLE
|
|
319
|
+
);
|
|
320
|
+
assert.match(error.message, /cannot disable a managed hook/u);
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await assert.rejects(prepareHelper({
|
|
325
|
+
provider: helperInventoryProvider({
|
|
326
|
+
records: [{
|
|
327
|
+
cwd: HELPER_EXECUTION_CWD,
|
|
328
|
+
errors: [{ message: "malformed hook configuration" }],
|
|
329
|
+
hooks: [],
|
|
330
|
+
warnings: []
|
|
331
|
+
}]
|
|
332
|
+
})
|
|
333
|
+
}), (error) => {
|
|
334
|
+
assert.equal(
|
|
335
|
+
error.code,
|
|
336
|
+
POLICY_UNENFORCEABLE
|
|
337
|
+
);
|
|
338
|
+
assert.match(error.message, /hook discovery has errors/u);
|
|
339
|
+
assert.doesNotMatch(JSON.stringify(error), /malformed hook configuration/u);
|
|
340
|
+
return true;
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("Codex helper inventories MCP servers independently of a large unrelated native model catalogue", async () => {
|
|
345
|
+
const provider = helperInventoryProvider({ mcpServers: { example: { command: "example" } } });
|
|
346
|
+
const read = provider.readConfig;
|
|
347
|
+
provider.readConfig = async (params) => {
|
|
348
|
+
const response = await read(params);
|
|
349
|
+
response.config.model_catalog = { notes: "catalogue".repeat(100_000) };
|
|
350
|
+
return response;
|
|
351
|
+
};
|
|
352
|
+
const prepared = await prepareHelper({
|
|
353
|
+
provider
|
|
354
|
+
});
|
|
355
|
+
assert.deepEqual(prepared.enforcement.mcpServerNames, ["example"]);
|
|
356
|
+
assert.equal(prepared.settings.config.mcp_servers.example.enabled, false);
|
|
357
|
+
assert.equal(prepared.settings.config.model_catalog, undefined);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("Codex helper bounds config and hook inventories without exposing their payloads", async () => {
|
|
361
|
+
const cases = [{
|
|
362
|
+
label: "too many MCP servers",
|
|
363
|
+
provider: helperInventoryProvider({
|
|
364
|
+
mcpServers: Object.fromEntries(Array.from({ length: 129 }, (_, index) => [
|
|
365
|
+
`server-${index}`,
|
|
366
|
+
{ command: "unsafe" }
|
|
367
|
+
]))
|
|
368
|
+
})
|
|
369
|
+
}, {
|
|
370
|
+
label: "oversized MCP configuration",
|
|
371
|
+
provider: helperInventoryProvider({
|
|
372
|
+
mcpServers: {
|
|
373
|
+
malicious: {
|
|
374
|
+
secret: `config-secret-${"x".repeat(300 * 1024)}`
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
}, {
|
|
379
|
+
label: "oversized hook inventory",
|
|
380
|
+
provider: helperInventoryProvider({
|
|
381
|
+
hooks: [{
|
|
382
|
+
currentHash: "hash",
|
|
383
|
+
enabled: false,
|
|
384
|
+
handlerType: "command",
|
|
385
|
+
isManaged: false,
|
|
386
|
+
key: "hook-key",
|
|
387
|
+
sourcePath: `hook-secret-${"x".repeat(520 * 1024)}`
|
|
388
|
+
}]
|
|
389
|
+
})
|
|
390
|
+
}];
|
|
391
|
+
|
|
392
|
+
for (const { label, provider } of cases) {
|
|
393
|
+
await assert.rejects(prepareHelper({
|
|
394
|
+
provider
|
|
395
|
+
}), (error) => {
|
|
396
|
+
assert.equal(
|
|
397
|
+
error.code,
|
|
398
|
+
POLICY_UNENFORCEABLE,
|
|
399
|
+
label
|
|
400
|
+
);
|
|
401
|
+
assert.doesNotMatch(JSON.stringify(error), /config-secret|hook-secret/u, label);
|
|
402
|
+
return true;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("Codex helper deletes a new thread when execution surfaces change during startup", async () => {
|
|
408
|
+
const calls = [];
|
|
409
|
+
let inventory = 0;
|
|
410
|
+
const provider = {
|
|
411
|
+
...helperExecutionProviderParts(),
|
|
412
|
+
currentConnectionGeneration() {
|
|
413
|
+
return 4;
|
|
414
|
+
},
|
|
415
|
+
currentServerInfo() {
|
|
416
|
+
return { userAgent: `vibe64/${MINIMUM_CODEX_VERSION} (unit test)` };
|
|
417
|
+
},
|
|
418
|
+
async deleteThread(threadId) {
|
|
419
|
+
calls.push(["deleteThread", threadId]);
|
|
420
|
+
},
|
|
421
|
+
async listHooks(cwds) {
|
|
422
|
+
calls.push(["listHooks", cwds]);
|
|
423
|
+
return {
|
|
424
|
+
data: [{
|
|
425
|
+
cwd: cwds[0],
|
|
426
|
+
errors: [],
|
|
427
|
+
hooks: [],
|
|
428
|
+
warnings: []
|
|
429
|
+
}]
|
|
430
|
+
};
|
|
431
|
+
},
|
|
432
|
+
async readConfig(params) {
|
|
433
|
+
inventory += 1;
|
|
434
|
+
calls.push(["readConfig", params]);
|
|
435
|
+
return {
|
|
436
|
+
config: {
|
|
437
|
+
mcp_servers: inventory === 1 ? {} : {
|
|
438
|
+
"late-write-tool": {
|
|
439
|
+
command: "write-anywhere"
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
},
|
|
445
|
+
async startThread(params) {
|
|
446
|
+
calls.push(["startThread", params]);
|
|
447
|
+
return {
|
|
448
|
+
id: "helper-thread"
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
await assert.rejects(startHelper({
|
|
454
|
+
provider
|
|
455
|
+
}), (error) => {
|
|
456
|
+
assert.equal(
|
|
457
|
+
error.code,
|
|
458
|
+
POLICY_UNENFORCEABLE
|
|
459
|
+
);
|
|
460
|
+
assert.match(error.message, /execution surfaces changed/u);
|
|
461
|
+
return true;
|
|
462
|
+
});
|
|
463
|
+
assert.deepEqual(calls.at(-1), ["deleteThread", "helper-thread"]);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("Codex helper reports retryable ownership when post-start deletion fails", async () => {
|
|
467
|
+
let inventory = 0;
|
|
468
|
+
const provider = helperInventoryProvider();
|
|
469
|
+
provider.readConfig = async () => {
|
|
470
|
+
inventory += 1;
|
|
471
|
+
return {
|
|
472
|
+
config: {
|
|
473
|
+
mcp_servers: inventory === 1 ? {} : {
|
|
474
|
+
"late-write-tool": {
|
|
475
|
+
command: "write-anywhere"
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
provider.startThread = async () => ({ id: "unclean-helper-thread" });
|
|
482
|
+
provider.deleteThread = async () => {
|
|
483
|
+
throw new Error("delete transport failed");
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
await assert.rejects(startHelper({
|
|
487
|
+
provider
|
|
488
|
+
}), (error) => {
|
|
489
|
+
assert.equal(
|
|
490
|
+
error.code,
|
|
491
|
+
POLICY_UNENFORCEABLE
|
|
492
|
+
);
|
|
493
|
+
assert.equal(error.codexAppServerHelperThreadCleanupRequired, true);
|
|
494
|
+
assert.equal(error.codexAppServerHelperThreadId, "unclean-helper-thread");
|
|
495
|
+
assert.equal(error.cleanupFailed, true);
|
|
496
|
+
assert.match(error.message, /could not retire a helper thread/u);
|
|
497
|
+
assert.equal(error.cause, undefined);
|
|
498
|
+
return true;
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("Codex helper safely reapplies isolation when resuming a controller-owned thread", async () => {
|
|
503
|
+
const calls = [];
|
|
504
|
+
const provider = {
|
|
505
|
+
...helperExecutionProviderParts(),
|
|
506
|
+
currentConnectionGeneration() {
|
|
507
|
+
return 7;
|
|
508
|
+
},
|
|
509
|
+
currentServerInfo() {
|
|
510
|
+
return { userAgent: `vibe64/${MINIMUM_CODEX_VERSION} (unit test)` };
|
|
511
|
+
},
|
|
512
|
+
async deleteThread(threadId) {
|
|
513
|
+
calls.push(["deleteThread", threadId]);
|
|
514
|
+
},
|
|
515
|
+
async listHooks(cwds) {
|
|
516
|
+
calls.push(["listHooks", cwds]);
|
|
517
|
+
return {
|
|
518
|
+
data: [{
|
|
519
|
+
cwd: cwds[0],
|
|
520
|
+
errors: [],
|
|
521
|
+
hooks: [],
|
|
522
|
+
warnings: []
|
|
523
|
+
}]
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
async readConfig(params) {
|
|
527
|
+
calls.push(["readConfig", params]);
|
|
528
|
+
return {
|
|
529
|
+
config: {
|
|
530
|
+
mcp_servers: {
|
|
531
|
+
filesystem: {
|
|
532
|
+
command: "unsafe-fixture"
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
},
|
|
538
|
+
async resumeThread(threadId, params) {
|
|
539
|
+
calls.push(["resumeThread", threadId, params]);
|
|
540
|
+
return {
|
|
541
|
+
id: threadId
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const result = await resumeHelper({
|
|
547
|
+
provider,
|
|
548
|
+
threadId: "registered-helper-thread"
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
assert.equal(result.threadId, "registered-helper-thread");
|
|
552
|
+
assert.equal(calls.filter(([method]) => method === "readConfig").length, 2);
|
|
553
|
+
assert.equal(calls.filter(([method]) => method === "listHooks").length, 2);
|
|
554
|
+
const resumeCall = calls.find(([method]) => method === "resumeThread");
|
|
555
|
+
assert.equal(resumeCall[1], "registered-helper-thread");
|
|
556
|
+
assert.equal(resumeCall[2].config.mcp_servers.filesystem.enabled, false);
|
|
557
|
+
assert.equal(calls.some(([method]) => method === "deleteThread"), false);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Supplementary ownership-boundary checks, alongside the original assertions.
|
|
561
|
+
test("helper inventory rejects a reconnect at either sequential read boundary", async () => {
|
|
562
|
+
for (const changedAt of ["readConfig", "listHooks"]) {
|
|
563
|
+
const provider = helperInventoryProvider();
|
|
564
|
+
let generation = 1;
|
|
565
|
+
provider.currentConnectionGeneration = () => generation;
|
|
566
|
+
const original = provider[changedAt];
|
|
567
|
+
provider[changedAt] = async (...args) => {
|
|
568
|
+
const result = await original(...args);
|
|
569
|
+
generation++;
|
|
570
|
+
return result;
|
|
571
|
+
};
|
|
572
|
+
await assert.rejects(prepareHelper({ provider }), /reconnected during helper policy verification/);
|
|
573
|
+
assert.deepEqual(provider.calls.map(([method]) => method),
|
|
574
|
+
changedAt === "readConfig" ? ["readConfig"] : ["readConfig", "listHooks"]);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("helper thread guards precede host preparation and preserve unsuccessful receipt ownership", async () => {
|
|
579
|
+
const calls = [];
|
|
580
|
+
const prepare = async () => { calls.push("prepare"); return { settings: {} }; };
|
|
581
|
+
await assert.rejects(isolation.start({ startThread() {} }, prepare), /cannot own and clean up/);
|
|
582
|
+
await assert.rejects(isolation.resume({ resumeThread() {} }, "thread", prepare), /cannot safely resume and clean up/);
|
|
583
|
+
await assert.rejects(isolation.resume({ resumeThread() {}, deleteThread() {} }, "", prepare), /controller-owned thread id/);
|
|
584
|
+
assert.deepEqual(calls, []);
|
|
585
|
+
await assert.rejects(isolation.start({
|
|
586
|
+
async startThread() { calls.push("start"); return {}; },
|
|
587
|
+
async deleteThread() { calls.push("delete"); }
|
|
588
|
+
}, prepare), /did not return a helper thread id/);
|
|
589
|
+
assert.deepEqual(calls, ["prepare", "start"]);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test("helper start and resume retire exact threads after account, generation, directory or hook changes", async () => {
|
|
593
|
+
for (const operation of ["start", "resume"]) {
|
|
594
|
+
for (const changed of ["account", "generation", "cwd", "hook"]) {
|
|
595
|
+
const provider = helperInventoryProvider();
|
|
596
|
+
let after = false;
|
|
597
|
+
const deleted = [];
|
|
598
|
+
provider.currentConnectionGeneration = () => after && changed === "generation" ? 2 : 1;
|
|
599
|
+
provider.currentHelperExecutionContext = async () => ({
|
|
600
|
+
accountIdentitySignature: after && changed === "account" ? `sha256:${"b".repeat(64)}` : HELPER_ACCOUNT_IDENTITY_SIGNATURE,
|
|
601
|
+
cwd: after && changed === "cwd" ? "/runtime/other-helper/workspace" : HELPER_EXECUTION_CWD,
|
|
602
|
+
executionMode: "helper"
|
|
603
|
+
});
|
|
604
|
+
const list = provider.listHooks;
|
|
605
|
+
provider.listHooks = async (...args) => {
|
|
606
|
+
const result = await list(...args);
|
|
607
|
+
if (after && changed === "hook") result.data[0].hooks.push({ key: "new-hook", enabled: false });
|
|
608
|
+
return result;
|
|
609
|
+
};
|
|
610
|
+
provider.deleteThread = async threadId => { deleted.push(threadId); };
|
|
611
|
+
provider.startThread = async () => { after = true; return { id: "exact-helper" }; };
|
|
612
|
+
provider.resumeThread = async threadId => { after = true; return { id: threadId }; };
|
|
613
|
+
await assert.rejects(operation === "start"
|
|
614
|
+
? startHelper({ provider })
|
|
615
|
+
: resumeHelper({ provider, threadId: "exact-helper" }), error => {
|
|
616
|
+
assert.match(error.message, /execution surfaces changed/);
|
|
617
|
+
assert.equal(error.codexAppServerHelperThreadId, "exact-helper");
|
|
618
|
+
assert.equal(error.codexAppServerHelperThreadRetired, true);
|
|
619
|
+
return true;
|
|
620
|
+
});
|
|
621
|
+
assert.deepEqual(deleted, ["exact-helper"], `${operation} ${changed}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("helper isolation requires an absolute dedicated account context before inventory", async () => {
|
|
627
|
+
for (const context of [
|
|
628
|
+
{ accountIdentitySignature: HELPER_ACCOUNT_IDENTITY_SIGNATURE, cwd: HELPER_EXECUTION_CWD, executionMode: "interactive" },
|
|
629
|
+
{ accountIdentitySignature: "unverified", cwd: HELPER_EXECUTION_CWD, executionMode: "helper" },
|
|
630
|
+
{ accountIdentitySignature: HELPER_ACCOUNT_IDENTITY_SIGNATURE, cwd: "relative", executionMode: "helper" }
|
|
631
|
+
]) {
|
|
632
|
+
const provider = helperInventoryProvider();
|
|
633
|
+
provider.currentHelperExecutionContext = async () => context;
|
|
634
|
+
await assert.rejects(prepareHelper({ provider }), /runtime isolation could not be verified/);
|
|
635
|
+
assert.deepEqual(provider.calls, []);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("common tool-free configuration uses verified inventories and a separate mutable policy copy", async () => {
|
|
640
|
+
const provider = helperInventoryProvider({
|
|
641
|
+
hooks: [{ key: "ambient", enabled: true }],
|
|
642
|
+
mcpServers: { files: { command: "ambient" } }
|
|
643
|
+
});
|
|
644
|
+
const configResult = await provider.readConfig({ cwd: HELPER_EXECUTION_CWD });
|
|
645
|
+
const hookResult = await provider.listHooks([HELPER_EXECUTION_CWD]);
|
|
646
|
+
const options = { configResult, hookResult, workdir: HELPER_EXECUTION_CWD };
|
|
647
|
+
const verified = isolation.configuration(options);
|
|
648
|
+
assert.equal(isolation.hasConfiguration(verified), true);
|
|
649
|
+
assert.equal(createCodexAppServerIsolation().hasConfiguration(verified), false);
|
|
650
|
+
assert.equal(Object.isFrozen(verified.hooks.state.ambient), true);
|
|
651
|
+
const common = codexToolFreeConfiguration(options);
|
|
652
|
+
assert.equal(isolation.hasConfiguration(common), false);
|
|
653
|
+
assert.deepEqual(common.mcp_servers, verified.mcp_servers);
|
|
654
|
+
assert.deepEqual(common.hooks, verified.hooks);
|
|
655
|
+
assert.equal(Object.hasOwn(common, "model_reasoning_summary"), false);
|
|
656
|
+
assert.equal(Object.hasOwn(common.features, "sleep_tool"), false);
|
|
657
|
+
common.features.goals = true;
|
|
658
|
+
common.shell_environment_policy.set.PATH = "/host/bin";
|
|
659
|
+
common.hooks.state.ambient.enabled = true;
|
|
660
|
+
assert.equal(verified.features.goals, false);
|
|
661
|
+
assert.deepEqual(verified.shell_environment_policy.set, {});
|
|
662
|
+
assert.equal(verified.hooks.state.ambient.enabled, false);
|
|
663
|
+
assert.throws(() => codexToolFreeConfiguration({
|
|
664
|
+
...options, hookResult: { data: [{ cwd: `${HELPER_EXECUTION_CWD}/../workspace`, hooks: [], errors: [] }] }
|
|
665
|
+
}), /incomplete hook inventory/);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("standalone helper version policy uses its own literal client name", () => {
|
|
669
|
+
const native = createCodexAppServerIsolation();
|
|
670
|
+
assert.deepEqual(native.assertCompatibility({ currentServerInfo: () => ({ userAgent: "jskit/0.151.0 (native)" }) }), {
|
|
671
|
+
minimumVersion: "0.151.0", version: "0.151.0"
|
|
672
|
+
});
|
|
673
|
+
const named = createCodexAppServerIsolation({ clientName: "host+integration", minimumVersion: "1.2.3" });
|
|
674
|
+
assert.deepEqual(named.assertCompatibility({ currentServerInfo: () => ({ userAgent: "host+integration/1.2.3" }) }), {
|
|
675
|
+
minimumVersion: "1.2.3", version: "1.2.3"
|
|
676
|
+
});
|
|
677
|
+
assert.throws(() => named.assertCompatibility({ currentServerInfo: () => ({ userAgent: "hostintegration/99.0.0" }) }), /unrecognised app-server version/);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test("ephemeral configuration reads inventories in parallel and preserves the original native tool settings", async () => {
|
|
681
|
+
const workdir = "/host/ephemeral";
|
|
682
|
+
const provider = helperInventoryProvider({
|
|
683
|
+
userAgent: `jskit/${MINIMUM_CODEX_VERSION}`,
|
|
684
|
+
hooks: [{ key: "ambient", enabled: true }],
|
|
685
|
+
mcpServers: { files: { command: "ambient" } }
|
|
686
|
+
});
|
|
687
|
+
provider.currentHelperExecutionContext = () => assert.fail("An ephemeral inventory does not select a Helper workspace.");
|
|
688
|
+
const release = Promise.withResolvers();
|
|
689
|
+
const readConfig = provider.readConfig;
|
|
690
|
+
provider.readConfig = async params => {
|
|
691
|
+
const result = await readConfig(params);
|
|
692
|
+
await release.promise;
|
|
693
|
+
return result;
|
|
694
|
+
};
|
|
695
|
+
const pending = readCodexToolFreeConfiguration(provider, workdir);
|
|
696
|
+
try {
|
|
697
|
+
assert.deepEqual(provider.calls, [
|
|
698
|
+
["readConfig", { cwd: workdir, includeLayers: false }],
|
|
699
|
+
["listHooks", [workdir]]
|
|
700
|
+
]);
|
|
701
|
+
} finally {
|
|
702
|
+
release.resolve();
|
|
703
|
+
}
|
|
704
|
+
const config = await pending;
|
|
705
|
+
// Native configuration assertions carried from the original Public
|
|
706
|
+
// non-project ephemeral conversation case; its thread policy stays there.
|
|
707
|
+
assert.equal(config.features.shell_tool, false);
|
|
708
|
+
assert.equal(config.features.web_search, undefined);
|
|
709
|
+
assert.equal(config.web_search, "disabled");
|
|
710
|
+
assert.deepEqual(config.mcp_servers, { files: { enabled: false } });
|
|
711
|
+
assert.deepEqual(config.hooks.state, { ambient: { enabled: false } });
|
|
712
|
+
assert.deepEqual(config.shell_environment_policy, { inherit: "none", set: {} });
|
|
713
|
+
assert.equal(Object.isFrozen(config), false);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test("ephemeral configuration preserves compatibility and positive-generation admission before reads", async () => {
|
|
717
|
+
const provider = helperInventoryProvider();
|
|
718
|
+
const policyError = new Error("Existing host compatibility policy rejected the provider.");
|
|
719
|
+
await assert.rejects(readCodexToolFreeConfiguration(provider, "/host/ephemeral", value => {
|
|
720
|
+
assert.equal(value, provider);
|
|
721
|
+
throw policyError;
|
|
722
|
+
}), error => error === policyError);
|
|
723
|
+
assert.deepEqual(provider.calls, []);
|
|
724
|
+
for (const missing of ["readConfig", "listHooks", "currentConnectionGeneration"]) {
|
|
725
|
+
const provider = helperInventoryProvider();
|
|
726
|
+
provider[missing] = undefined;
|
|
727
|
+
await assert.rejects(readCodexToolFreeConfiguration(provider, "/host/ephemeral", isolation.assertCompatibility), /cannot verify tool isolation/);
|
|
728
|
+
assert.deepEqual(provider.calls, []);
|
|
729
|
+
}
|
|
730
|
+
for (const generation of [0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1]) {
|
|
731
|
+
const provider = helperInventoryProvider({ generation });
|
|
732
|
+
await assert.rejects(readCodexToolFreeConfiguration(provider, "/host/ephemeral", isolation.assertCompatibility), /no active connection/);
|
|
733
|
+
assert.deepEqual(provider.calls, []);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("ephemeral configuration rejects either read's connection change and preserves native read errors", async () => {
|
|
738
|
+
for (const changedAt of ["readConfig", "listHooks"]) {
|
|
739
|
+
const provider = helperInventoryProvider();
|
|
740
|
+
let generation = 1;
|
|
741
|
+
provider.currentConnectionGeneration = () => generation;
|
|
742
|
+
const read = provider[changedAt];
|
|
743
|
+
provider[changedAt] = async (...args) => {
|
|
744
|
+
const result = await read(...args);
|
|
745
|
+
generation++;
|
|
746
|
+
return result;
|
|
747
|
+
};
|
|
748
|
+
await assert.rejects(readCodexToolFreeConfiguration(provider, "/host/ephemeral", isolation.assertCompatibility), /reconnected while verifying/);
|
|
749
|
+
assert.deepEqual(provider.calls.map(([method]) => method), ["readConfig", "listHooks"]);
|
|
750
|
+
}
|
|
751
|
+
const provider = helperInventoryProvider();
|
|
752
|
+
const readError = new Error("Native configuration read failed.");
|
|
753
|
+
provider.readConfig = async () => { throw readError; };
|
|
754
|
+
await assert.rejects(readCodexToolFreeConfiguration(provider, "/host/ephemeral", isolation.assertCompatibility), error => error === readError);
|
|
755
|
+
assert.deepEqual(provider.calls, [["listHooks", ["/host/ephemeral"]]]);
|
|
756
|
+
});
|