@farmslot/agent-runtime 0.9.0 → 0.11.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/CHANGELOG.md +20 -1
- package/README.md +1 -1
- package/bin/farmslot-agent.mjs +1 -1
- package/dist/execution-template/create.d.ts.map +1 -1
- package/dist/execution-template/create.js +4 -9
- package/dist/execution-template/create.js.map +1 -1
- package/dist/execution-template/execution-template.test.js +85 -34
- package/dist/execution-template/execution-template.test.js.map +1 -1
- package/dist/execution-template/frontmatter.d.ts +6 -3
- package/dist/execution-template/frontmatter.d.ts.map +1 -1
- package/dist/execution-template/frontmatter.js +8 -10
- package/dist/execution-template/frontmatter.js.map +1 -1
- package/dist/execution-template/index.d.ts +2 -2
- package/dist/execution-template/index.d.ts.map +1 -1
- package/dist/execution-template/index.js +2 -2
- package/dist/execution-template/index.js.map +1 -1
- package/dist/execution-template/infer.d.ts +2 -3
- package/dist/execution-template/infer.d.ts.map +1 -1
- package/dist/execution-template/infer.js +2 -19
- package/dist/execution-template/infer.js.map +1 -1
- package/dist/execution-template/lint.d.ts.map +1 -1
- package/dist/execution-template/lint.js +12 -14
- package/dist/execution-template/lint.js.map +1 -1
- package/dist/execution-template/resolve.d.ts.map +1 -1
- package/dist/execution-template/resolve.js +0 -2
- package/dist/execution-template/resolve.js.map +1 -1
- package/dist/execution-template/select.d.ts.map +1 -1
- package/dist/execution-template/select.js +4 -8
- package/dist/execution-template/select.js.map +1 -1
- package/dist/execution-template/snapshot.d.ts.map +1 -1
- package/dist/execution-template/snapshot.js +0 -1
- package/dist/execution-template/snapshot.js.map +1 -1
- package/dist/execution-template/types.d.ts +10 -5
- package/dist/execution-template/types.d.ts.map +1 -1
- package/dist/execution-template/types.js.map +1 -1
- package/dist/native/account-claude.d.ts +5 -0
- package/dist/native/account-claude.d.ts.map +1 -0
- package/dist/native/account-claude.js +42 -0
- package/dist/native/account-claude.js.map +1 -0
- package/dist/native/account-codex.d.ts +4 -0
- package/dist/native/account-codex.d.ts.map +1 -0
- package/dist/native/account-codex.js +51 -0
- package/dist/native/account-codex.js.map +1 -0
- package/dist/native/account-common.d.ts +30 -0
- package/dist/native/account-common.d.ts.map +1 -0
- package/dist/native/account-common.js +155 -0
- package/dist/native/account-common.js.map +1 -0
- package/dist/native/account-cursor.d.ts +5 -0
- package/dist/native/account-cursor.d.ts.map +1 -0
- package/dist/native/account-cursor.js +48 -0
- package/dist/native/account-cursor.js.map +1 -0
- package/dist/native/account-grok.d.ts +4 -0
- package/dist/native/account-grok.d.ts.map +1 -0
- package/dist/native/account-grok.js +55 -0
- package/dist/native/account-grok.js.map +1 -0
- package/dist/native/account-observation.test.d.ts +2 -0
- package/dist/native/account-observation.test.d.ts.map +1 -0
- package/dist/native/account-observation.test.js +256 -0
- package/dist/native/account-observation.test.js.map +1 -0
- package/dist/native/account-profiles.d.ts +11 -0
- package/dist/native/account-profiles.d.ts.map +1 -0
- package/dist/native/account-profiles.js +172 -0
- package/dist/native/account-profiles.js.map +1 -0
- package/dist/native/account-profiles.test.d.ts +2 -0
- package/dist/native/account-profiles.test.d.ts.map +1 -0
- package/dist/native/account-profiles.test.js +172 -0
- package/dist/native/account-profiles.test.js.map +1 -0
- package/dist/native/account-types.d.ts +11 -0
- package/dist/native/account-types.d.ts.map +1 -0
- package/dist/native/account-types.js +2 -0
- package/dist/native/account-types.js.map +1 -0
- package/dist/native/acp-requests.d.ts +11 -0
- package/dist/native/acp-requests.d.ts.map +1 -0
- package/dist/native/acp-requests.js +109 -0
- package/dist/native/acp-requests.js.map +1 -0
- package/dist/native/acp-rpc.d.ts +15 -0
- package/dist/native/acp-rpc.d.ts.map +1 -0
- package/dist/native/acp-rpc.js +73 -0
- package/dist/native/acp-rpc.js.map +1 -0
- package/dist/native/acp.d.ts +15 -0
- package/dist/native/acp.d.ts.map +1 -0
- package/dist/native/acp.js +285 -0
- package/dist/native/acp.js.map +1 -0
- package/dist/native/acp.test.d.ts +2 -0
- package/dist/native/acp.test.d.ts.map +1 -0
- package/dist/native/acp.test.js +257 -0
- package/dist/native/acp.test.js.map +1 -0
- package/dist/native/claude.d.ts.map +1 -1
- package/dist/native/claude.js +14 -0
- package/dist/native/claude.js.map +1 -1
- package/dist/native/client.d.ts +13 -0
- package/dist/native/client.d.ts.map +1 -1
- package/dist/native/client.js +112 -1
- package/dist/native/client.js.map +1 -1
- package/dist/native/codex.d.ts.map +1 -1
- package/dist/native/codex.js +61 -3
- package/dist/native/codex.js.map +1 -1
- package/dist/native/cursor-model.d.ts +6 -0
- package/dist/native/cursor-model.d.ts.map +1 -0
- package/dist/native/cursor-model.js +74 -0
- package/dist/native/cursor-model.js.map +1 -0
- package/dist/native/cursor-model.test.d.ts +2 -0
- package/dist/native/cursor-model.test.d.ts.map +1 -0
- package/dist/native/cursor-model.test.js +40 -0
- package/dist/native/cursor-model.test.js.map +1 -0
- package/dist/native/cursor.d.ts +2 -0
- package/dist/native/cursor.d.ts.map +1 -0
- package/dist/native/cursor.js +30 -0
- package/dist/native/cursor.js.map +1 -0
- package/dist/native/durability.test.js +461 -1
- package/dist/native/durability.test.js.map +1 -1
- package/dist/native/filesystem-policy.test.d.ts +2 -0
- package/dist/native/filesystem-policy.test.d.ts.map +1 -0
- package/dist/native/filesystem-policy.test.js +136 -0
- package/dist/native/filesystem-policy.test.js.map +1 -0
- package/dist/native/grok.d.ts +2 -0
- package/dist/native/grok.d.ts.map +1 -0
- package/dist/native/grok.js +17 -0
- package/dist/native/grok.js.map +1 -0
- package/dist/native/host.js +39 -0
- package/dist/native/host.js.map +1 -1
- package/dist/native/index.d.ts +3 -0
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +3 -0
- package/dist/native/index.js.map +1 -1
- package/dist/native/ipc.d.ts +59 -0
- package/dist/native/ipc.d.ts.map +1 -1
- package/dist/native/ipc.js +76 -4
- package/dist/native/ipc.js.map +1 -1
- package/dist/native/manager.d.ts +22 -4
- package/dist/native/manager.d.ts.map +1 -1
- package/dist/native/manager.js +386 -46
- package/dist/native/manager.js.map +1 -1
- package/dist/native/process-tree.d.ts +11 -0
- package/dist/native/process-tree.d.ts.map +1 -1
- package/dist/native/process-tree.js +123 -20
- package/dist/native/process-tree.js.map +1 -1
- package/dist/native/process-tree.test.js +126 -1
- package/dist/native/process-tree.test.js.map +1 -1
- package/dist/native/process.d.ts +5 -1
- package/dist/native/process.d.ts.map +1 -1
- package/dist/native/process.js +36 -13
- package/dist/native/process.js.map +1 -1
- package/dist/native/process.test.d.ts +2 -0
- package/dist/native/process.test.d.ts.map +1 -0
- package/dist/native/process.test.js +56 -0
- package/dist/native/process.test.js.map +1 -0
- package/dist/native/profile-service.d.ts +5 -0
- package/dist/native/profile-service.d.ts.map +1 -0
- package/dist/native/profile-service.js +76 -0
- package/dist/native/profile-service.js.map +1 -0
- package/dist/native/registry.d.ts +20 -0
- package/dist/native/registry.d.ts.map +1 -0
- package/dist/native/registry.js +89 -0
- package/dist/native/registry.js.map +1 -0
- package/dist/native/registry.test.d.ts +2 -0
- package/dist/native/registry.test.d.ts.map +1 -0
- package/dist/native/registry.test.js +13 -0
- package/dist/native/registry.test.js.map +1 -0
- package/dist/native/review-sandbox.d.ts +12 -0
- package/dist/native/review-sandbox.d.ts.map +1 -0
- package/dist/native/review-sandbox.js +6 -0
- package/dist/native/review-sandbox.js.map +1 -0
- package/dist/native/review-sandbox.test.d.ts +2 -0
- package/dist/native/review-sandbox.test.d.ts.map +1 -0
- package/dist/native/review-sandbox.test.js +40 -0
- package/dist/native/review-sandbox.test.js.map +1 -0
- package/dist/native/service.d.ts.map +1 -1
- package/dist/native/service.js +42 -8
- package/dist/native/service.js.map +1 -1
- package/dist/native/supervisor.js +121 -33
- package/dist/native/supervisor.js.map +1 -1
- package/dist/native/types.d.ts +11 -1
- package/dist/native/types.d.ts.map +1 -1
- package/dist/native/worker-history.d.ts +16 -0
- package/dist/native/worker-history.d.ts.map +1 -0
- package/dist/native/worker-history.js +77 -0
- package/dist/native/worker-history.js.map +1 -0
- package/dist/native/worker-history.test.d.ts +2 -0
- package/dist/native/worker-history.test.d.ts.map +1 -0
- package/dist/native/worker-history.test.js +195 -0
- package/dist/native/worker-history.test.js.map +1 -0
- package/dist/native/worker-launch.d.ts +72 -0
- package/dist/native/worker-launch.d.ts.map +1 -0
- package/dist/native/worker-launch.js +134 -0
- package/dist/native/worker-launch.js.map +1 -0
- package/dist/native/worker-launch.test.d.ts +2 -0
- package/dist/native/worker-launch.test.d.ts.map +1 -0
- package/dist/native/worker-launch.test.js +61 -0
- package/dist/native/worker-launch.test.js.map +1 -0
- package/dist/native/worker-relocation.test.d.ts +2 -0
- package/dist/native/worker-relocation.test.d.ts.map +1 -0
- package/dist/native/worker-relocation.test.js +110 -0
- package/dist/native/worker-relocation.test.js.map +1 -0
- package/dist/native/worker-service.d.ts +4 -0
- package/dist/native/worker-service.d.ts.map +1 -0
- package/dist/native/worker-service.js +85 -0
- package/dist/native/worker-service.js.map +1 -0
- package/dist/task-init/discover.d.ts +26 -0
- package/dist/task-init/discover.d.ts.map +1 -0
- package/dist/task-init/discover.js +64 -0
- package/dist/task-init/discover.js.map +1 -0
- package/dist/task-init/discover.test.d.ts +2 -0
- package/dist/task-init/discover.test.d.ts.map +1 -0
- package/dist/task-init/discover.test.js +58 -0
- package/dist/task-init/discover.test.js.map +1 -0
- package/dist/task-init/index.d.ts +6 -1
- package/dist/task-init/index.d.ts.map +1 -1
- package/dist/task-init/index.js +8 -3
- package/dist/task-init/index.js.map +1 -1
- package/dist/task-init/task-document.d.ts.map +1 -1
- package/dist/task-init/task-document.js +2 -2
- package/dist/task-init/task-document.js.map +1 -1
- package/dist/task-init/write.d.ts +0 -6
- package/dist/task-init/write.d.ts.map +1 -1
- package/dist/task-init/write.js +1 -8
- package/dist/task-init/write.js.map +1 -1
- package/package.json +3 -3
- package/scripts/cursor-session-discovery.cjs +48 -0
- package/scripts/execution-template-cli.mjs +8 -11
- package/scripts/review-filesystem.cjs +39 -0
- package/scripts/review-session.cjs +45 -0
- package/scripts/review-terminal.cjs +134 -0
- package/scripts/task-init-cli.mjs +3 -3
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { configureCursorModel } from './cursor-model.js';
|
|
4
|
+
for (const [alias, model, parameter, value] of [
|
|
5
|
+
['cursor-grok-4.6-xhigh', 'grok-4.6', 'effort', 'xhigh'],
|
|
6
|
+
['composer-2.5-fast', 'composer-2.5', 'fast', 'true'],
|
|
7
|
+
['claude-opus-5-thinking-high', 'claude-opus-5', 'thinking', 'true'],
|
|
8
|
+
['claude-4.6-opus-high-thinking', 'claude-opus-4-6', 'effort', 'high'],
|
|
9
|
+
['gpt-5.6-sol[reasoning=high]', 'gpt-5.6-sol', 'reasoning', 'high'],
|
|
10
|
+
['auto', 'auto-smart', 'model', 'auto-smart'],
|
|
11
|
+
])
|
|
12
|
+
test(`Cursor confirms the native selection for ${alias}`, async () => {
|
|
13
|
+
const option = (id, values, category = '') => ({
|
|
14
|
+
id,
|
|
15
|
+
category,
|
|
16
|
+
currentValue: values[0],
|
|
17
|
+
options: values.map((value) => ({ value })),
|
|
18
|
+
});
|
|
19
|
+
const choices = [
|
|
20
|
+
option('model', [model]),
|
|
21
|
+
option('thinking', ['false', 'true'], 'thought_level'),
|
|
22
|
+
option('effort', ['medium', 'high', 'xhigh'], 'thought_level'),
|
|
23
|
+
option('reasoning', ['medium', 'high'], 'thought_level'),
|
|
24
|
+
option('fast', ['false', 'true']),
|
|
25
|
+
];
|
|
26
|
+
await configureCursorModel('session', { configOptions: choices }, alias, async (method, params) => {
|
|
27
|
+
assert.equal(method, 'session/set_config_option');
|
|
28
|
+
assert.equal(params.sessionId, 'session');
|
|
29
|
+
const selected = choices.find((choice) => choice.id === params.configId);
|
|
30
|
+
assert(selected.options.some((option) => option.value === params.value));
|
|
31
|
+
selected.currentValue = String(params.value);
|
|
32
|
+
return { configOptions: structuredClone(choices) };
|
|
33
|
+
});
|
|
34
|
+
assert.equal(choices.find((choice) => choice.id === 'model').currentValue, model);
|
|
35
|
+
assert.equal(choices.find((choice) => choice.id === parameter).currentValue, value);
|
|
36
|
+
});
|
|
37
|
+
test('Cursor refuses a model switch that the native API did not confirm', async () => {
|
|
38
|
+
await assert.rejects(configureCursorModel('session', { configOptions: [{ id: 'model', options: [{ value: 'requested' }] }] }, 'requested', async () => ({ configOptions: [{ id: 'model', currentValue: 'other' }] })), /did not select/);
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=cursor-model.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-model.test.js","sourceRoot":"","sources":["../../src/native/cursor-model.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,IAAI;IAC7C,CAAC,uBAAuB,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC;IACxD,CAAC,mBAAmB,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,CAAC;IACrD,CAAC,6BAA6B,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,CAAC;IACpE,CAAC,+BAA+B,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,CAAC;IACtE,CAAC,6BAA6B,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,CAAC;IACnE,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,CAAC;CAC9C;IACC,IAAI,CAAC,4CAA4C,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE;QACnE,MAAM,MAAM,GAAG,CAAC,EAAU,EAAE,MAAgB,EAAE,QAAQ,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;YAC/D,EAAE;YACF,QAAQ;YACR,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;YACvB,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;SAC5C,CAAC,CAAC;QACH,MAAM,OAAO,GAAG;YACd,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;YACxB,MAAM,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,eAAe,CAAC;YACtD,MAAM,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,CAAC;YAC9D,MAAM,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,eAAe,CAAC;YACxD,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;SAClC,CAAC;QACF,MAAM,oBAAoB,CACxB,SAAS,EACT,EAAE,aAAa,EAAE,OAAO,EAAE,EAC1B,KAAK,EACL,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACvB,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;YAClD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YAC1C,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,QAAQ,CAAE,CAAC;YAC1E,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACzE,QAAQ,CAAC,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC7C,OAAO,EAAE,aAAa,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;QACrD,CAAC,CACF,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,OAAO,CAAE,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;QACnF,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,SAAS,CAAE,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACvF,CAAC,CAAC,CAAC;AAEL,IAAI,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;IACnF,MAAM,MAAM,CAAC,OAAO,CAClB,oBAAoB,CAClB,SAAS,EACT,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,EAAE,EACvE,WAAW,EACX,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAC1E,EACD,gBAAgB,CACjB,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/native/cursor.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,mBAAmB,oCAwB9B,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createAcpAdapter } from './acp.js';
|
|
2
|
+
import { cursorRequest } from './acp-requests.js';
|
|
3
|
+
import { configureCursorModel } from './cursor-model.js';
|
|
4
|
+
export const cursorNativeAdapter = createAcpAdapter({
|
|
5
|
+
authMethod: 'cursor_login',
|
|
6
|
+
clientCapabilities: { _meta: { parameterizedModelPicker: true } },
|
|
7
|
+
configureModel: configureCursorModel,
|
|
8
|
+
defaultModeId: 'agent',
|
|
9
|
+
args(options) {
|
|
10
|
+
if (options.effort)
|
|
11
|
+
throw new Error('Cursor effort must be configured in the native model ID');
|
|
12
|
+
return [
|
|
13
|
+
...(options.filesystemPolicy
|
|
14
|
+
? [
|
|
15
|
+
'--trust',
|
|
16
|
+
'--sandbox',
|
|
17
|
+
'enabled',
|
|
18
|
+
...options.filesystemPolicy.writableRoots.flatMap((root) => ['--add-dir', root]),
|
|
19
|
+
]
|
|
20
|
+
: []),
|
|
21
|
+
...(options.processSandbox || (options.safetyTier && options.safetyTier !== 'sandboxed')
|
|
22
|
+
? ['--force']
|
|
23
|
+
: []),
|
|
24
|
+
'acp',
|
|
25
|
+
];
|
|
26
|
+
},
|
|
27
|
+
modes: ['default'],
|
|
28
|
+
extensionRequest: cursorRequest,
|
|
29
|
+
});
|
|
30
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/native/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;IAClD,UAAU,EAAE,cAAc;IAC1B,kBAAkB,EAAE,EAAE,KAAK,EAAE,EAAE,wBAAwB,EAAE,IAAI,EAAE,EAAE;IACjE,cAAc,EAAE,oBAAoB;IACpC,aAAa,EAAE,OAAO;IACtB,IAAI,CAAC,OAAO;QACV,IAAI,OAAO,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC/F,OAAO;YACL,GAAG,CAAC,OAAO,CAAC,gBAAgB;gBAC1B,CAAC,CAAC;oBACE,SAAS;oBACT,WAAW;oBACX,SAAS;oBACT,GAAG,OAAO,CAAC,gBAAgB,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;iBACjF;gBACH,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,OAAO,CAAC,cAAc,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,UAAU,KAAK,WAAW,CAAC;gBACtF,CAAC,CAAC,CAAC,SAAS,CAAC;gBACb,CAAC,CAAC,EAAE,CAAC;YACP,KAAK;SACN,CAAC;IACJ,CAAC;IACD,KAAK,EAAE,CAAC,SAAS,CAAC;IAClB,gBAAgB,EAAE,aAAa;CAChC,CAAC,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
2
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { once } from 'node:events';
|
|
5
5
|
import fs, { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
@@ -12,6 +12,7 @@ import { NativeSessionClient } from './client.js';
|
|
|
12
12
|
import { decodeRequest, requestHost, socketDirectory } from './ipc.js';
|
|
13
13
|
import { NativeSessionManager } from './manager.js';
|
|
14
14
|
import { alive, appendDurable, matchesProcess, privateDirectory, readJson } from './storage.js';
|
|
15
|
+
import { decodeNativeWorkerLaunch } from './worker-launch.js';
|
|
15
16
|
const fixture = `#!/usr/bin/env node
|
|
16
17
|
if (process.argv.includes('--version')) { console.log('fixture 1'); process.exit(0); }
|
|
17
18
|
const fs = require('node:fs');
|
|
@@ -21,10 +22,12 @@ require('node:readline').createInterface({input:process.stdin}).on('line', line
|
|
|
21
22
|
const m = JSON.parse(line);
|
|
22
23
|
if(m.method==='initialize') send({id:m.id,result:{}});
|
|
23
24
|
if(m.method==='thread/start'||m.method==='thread/resume') {
|
|
25
|
+
fs.writeFileSync('native-start.json',JSON.stringify({params:m.params,env:{probe:process.env.NATIVE_WORKER_PROBE,omc:process.env.DISABLE_OMC,omx:process.env.DISABLE_OMX}}),{mode:0o600});
|
|
24
26
|
thread=m.params.threadId || 'thread-'+process.pid;
|
|
25
27
|
send({id:m.id,result:{thread:{id:thread}}});
|
|
26
28
|
}
|
|
27
29
|
if(m.method==='turn/start') {
|
|
30
|
+
fs.writeFileSync('native-turn.json',JSON.stringify(m.params),{mode:0o600});
|
|
28
31
|
const text=m.params.input[0].text; turn='turn-'+m.id;
|
|
29
32
|
fs.appendFileSync('inputs',text+'\\n');
|
|
30
33
|
if(text==='unknown') { process.exit(23); }
|
|
@@ -70,6 +73,95 @@ function setup() {
|
|
|
70
73
|
},
|
|
71
74
|
};
|
|
72
75
|
}
|
|
76
|
+
for (const mode of ['startup', 'periodic', 'persistent']) {
|
|
77
|
+
test(`supervisor ${mode} census failure stops its host and preserves cleanup truth`, async () => {
|
|
78
|
+
const fixture = setup();
|
|
79
|
+
const client = new NativeSessionClient(fixture.root);
|
|
80
|
+
const config = join(fixture.cwd, 'census-fault.json');
|
|
81
|
+
const previousOptions = process.env.NODE_OPTIONS;
|
|
82
|
+
const previousFault = process.env.FARMSLOT_NATIVE_CENSUS_FAULT;
|
|
83
|
+
const preload = new URL('../../../../scripts/runner-validation/fixtures/native-census-fault.mjs', import.meta.url).href;
|
|
84
|
+
process.env.NODE_OPTIONS = `${previousOptions ?? ''} --import=${preload}`.trim();
|
|
85
|
+
process.env.FARMSLOT_NATIVE_CENSUS_FAULT = config;
|
|
86
|
+
writeFileSync(config, JSON.stringify({ root: fixture.root, armed: mode === 'startup' }), {
|
|
87
|
+
mode: 0o600,
|
|
88
|
+
});
|
|
89
|
+
writeFileSync(join(fixture.cwd, 'codex'), readFileSync(join(fixture.cwd, 'codex'), 'utf8').replace('},600);', '},60000);'));
|
|
90
|
+
let descendant;
|
|
91
|
+
let descendantStart;
|
|
92
|
+
try {
|
|
93
|
+
if (mode === 'startup') {
|
|
94
|
+
await assert.rejects(client.create('owner', { runner: 'codex', cwd: fixture.cwd }), /Native host unavailable/);
|
|
95
|
+
const host = readJson(join(fixture.root, 'host.json'));
|
|
96
|
+
const worker = readJson(join(fixture.root, 'worker.json'));
|
|
97
|
+
assert.equal(alive(host.pid), false);
|
|
98
|
+
assert.equal(alive(worker.pid), false);
|
|
99
|
+
assert.equal(readJson(join(fixture.root, 'cleanup.json')).state, 'complete');
|
|
100
|
+
const retry = await client.create('owner', { runner: 'codex', cwd: fixture.cwd });
|
|
101
|
+
await client.close('owner', retry.id);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const session = await client.create('owner', { runner: 'codex', cwd: fixture.cwd });
|
|
105
|
+
const host = readJson(join(fixture.root, 'host.json'));
|
|
106
|
+
await client.send('owner', session.id, 'census', 'crash');
|
|
107
|
+
await until(() => existsSync(join(fixture.cwd, 'descendant')));
|
|
108
|
+
descendant = Number(readFileSync(join(fixture.cwd, 'descendant'), 'utf8'));
|
|
109
|
+
descendantStart = execFileSync('ps', ['-p', String(descendant), '-o', 'lstart='], {
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
}).trim();
|
|
112
|
+
writeFileSync(config, JSON.stringify({ root: fixture.root, armed: false, observePid: descendant }));
|
|
113
|
+
await until(() => existsSync(`${config}.observed`));
|
|
114
|
+
writeFileSync(config, JSON.stringify({ root: fixture.root, armed: true, persistent: mode === 'persistent' }));
|
|
115
|
+
await until(() => existsSync(`${config}.fired`) && !alive(host.pid) && !alive(session.hostPid));
|
|
116
|
+
await until(() => !alive(session.processPid));
|
|
117
|
+
const cleanup = readJson(join(fixture.root, 'cleanup.json'));
|
|
118
|
+
assert.equal(cleanup.state, mode === 'persistent' ? 'unknown' : 'complete');
|
|
119
|
+
assert.match(cleanup.error ?? '', /census timeout/);
|
|
120
|
+
assert.equal(existsSync(join(fixture.cwd, 'effects')), false);
|
|
121
|
+
if (mode === 'persistent') {
|
|
122
|
+
assert.ok(cleanup.observedProcesses?.some((entry) => entry.pid === descendant));
|
|
123
|
+
await assert.rejects(client.list('owner'), /Native host unavailable/);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
assert.equal(alive(descendant), false);
|
|
127
|
+
const recovered = await client.read('owner', session.id);
|
|
128
|
+
assert.equal(recovered.session.state, 'failed');
|
|
129
|
+
assert.equal(recovered.session.processStopped, true);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
rmSync(config, { force: true });
|
|
135
|
+
const hostFile = join(fixture.root, 'host.json');
|
|
136
|
+
if (existsSync(hostFile)) {
|
|
137
|
+
const host = readJson(hostFile);
|
|
138
|
+
if (alive(host.pid)) {
|
|
139
|
+
process.kill(host.pid, 'SIGTERM');
|
|
140
|
+
await until(() => !alive(host.pid));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (descendant && alive(descendant)) {
|
|
144
|
+
const ownedDescendant = descendant;
|
|
145
|
+
const current = execFileSync('ps', ['-p', String(ownedDescendant), '-o', 'lstart='], {
|
|
146
|
+
encoding: 'utf8',
|
|
147
|
+
}).trim();
|
|
148
|
+
assert.equal(current, descendantStart, 'Fixture descendant PID was reused');
|
|
149
|
+
process.kill(ownedDescendant, 'SIGKILL');
|
|
150
|
+
await until(() => !alive(ownedDescendant));
|
|
151
|
+
}
|
|
152
|
+
if (previousOptions === undefined)
|
|
153
|
+
delete process.env.NODE_OPTIONS;
|
|
154
|
+
else
|
|
155
|
+
process.env.NODE_OPTIONS = previousOptions;
|
|
156
|
+
if (previousFault === undefined)
|
|
157
|
+
delete process.env.FARMSLOT_NATIVE_CENSUS_FAULT;
|
|
158
|
+
else
|
|
159
|
+
process.env.FARMSLOT_NATIVE_CENSUS_FAULT = previousFault;
|
|
160
|
+
rmSync(socketDirectory(fixture.root), { recursive: true, force: true });
|
|
161
|
+
fixture.restore();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
73
165
|
test('reserved native creation is concurrent-safe and survives journal reload without relaunch', async () => {
|
|
74
166
|
const fixture = setup();
|
|
75
167
|
const manager = new NativeSessionManager(fixture.root);
|
|
@@ -172,6 +264,336 @@ test('an uncertain reservation fsync remains failed after retry and journal relo
|
|
|
172
264
|
fixture.restore();
|
|
173
265
|
}
|
|
174
266
|
});
|
|
267
|
+
test('worker launch keeps environment private and transfers only an idle task lease', async () => {
|
|
268
|
+
const fixture = setup();
|
|
269
|
+
const manager = new NativeSessionManager(fixture.root);
|
|
270
|
+
const secret = `private-environment-${randomUUID()}`;
|
|
271
|
+
const launch = {
|
|
272
|
+
leaseId: randomUUID(),
|
|
273
|
+
executable: './codex',
|
|
274
|
+
effort: 'high',
|
|
275
|
+
safetyTier: 'full-auto',
|
|
276
|
+
environment: { set: { NATIVE_WORKER_PROBE: secret }, unset: [] },
|
|
277
|
+
};
|
|
278
|
+
const params = { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd };
|
|
279
|
+
try {
|
|
280
|
+
assert.throws(() => decodeNativeWorkerLaunch({ ...launch, futurePermissionFlag: true }), /Unsupported/);
|
|
281
|
+
const session = await manager.ensure('owner', params, launch);
|
|
282
|
+
assert.equal(session.workerManaged, true);
|
|
283
|
+
assert.equal(session.workerLeaseId, launch.leaseId);
|
|
284
|
+
const started = readJson(join(fixture.cwd, 'native-start.json'));
|
|
285
|
+
assert.deepEqual(started.env, { probe: secret, omc: '1', omx: '1' });
|
|
286
|
+
assert.equal(started.params.approvalPolicy, 'never');
|
|
287
|
+
assert.equal(started.params.sandbox, 'workspace-write');
|
|
288
|
+
const next = { ...launch, leaseId: randomUUID() };
|
|
289
|
+
await manager.send('owner', session.id, randomUUID(), 'approval');
|
|
290
|
+
assert.equal(readJson(join(fixture.cwd, 'native-turn.json')).effort, 'high');
|
|
291
|
+
await until(() => manager.read('owner', session.id).pendingRequests.length > 0);
|
|
292
|
+
assert.throws(() => manager.transferWorker('owner', session.id, session.generation, launch.leaseId, next), /idle session/);
|
|
293
|
+
await manager.respond('owner', session.id, manager.read('owner', session.id).pendingRequests[0].request.id, { decision: 'deny' });
|
|
294
|
+
await until(() => manager.read('owner', session.id).session.state === 'idle');
|
|
295
|
+
assert.throws(() => manager.transferWorker('owner', session.id, session.generation, launch.leaseId, {
|
|
296
|
+
...next,
|
|
297
|
+
safetyTier: 'dangerous',
|
|
298
|
+
}), /preserve generation and launch configuration/);
|
|
299
|
+
const handedOff = manager.transferWorker('owner', session.id, session.generation, launch.leaseId, next);
|
|
300
|
+
assert.equal(handedOff.workerLeaseId, next.leaseId);
|
|
301
|
+
assert.equal(handedOff.processPid, session.processPid);
|
|
302
|
+
assert.equal(manager.transferWorker('owner', session.id, session.generation, launch.leaseId, next)
|
|
303
|
+
.workerLeaseId, next.leaseId);
|
|
304
|
+
assert.throws(() => manager.assertWorkerLease('owner', session.id, session.generation, launch.leaseId), /task lease changed/);
|
|
305
|
+
await assert.rejects(manager.ensure('owner', params, launch), /another worker task lease/);
|
|
306
|
+
manager.assertWorkerLease('owner', session.id, session.generation, next.leaseId);
|
|
307
|
+
assert.equal((await manager.ensure('owner', params, next)).processPid, session.processPid);
|
|
308
|
+
assert.equal(readFileSync(join(fixture.root, `${session.id}.journal`), 'utf8').includes(secret), false);
|
|
309
|
+
await manager.close('owner', session.id);
|
|
310
|
+
await assert.rejects(manager.create('owner', {
|
|
311
|
+
runner: 'codex',
|
|
312
|
+
cwd: fixture.cwd,
|
|
313
|
+
resumeSessionId: session.nativeSessionId,
|
|
314
|
+
}), /Worker recovery requires/);
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
for (const session of manager.list('owner'))
|
|
318
|
+
await manager.close('owner', session.id);
|
|
319
|
+
fixture.restore();
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
test('uncertain worker lease persistence refuses input until explicit close and recovery', async (t) => {
|
|
323
|
+
const fixture = setup();
|
|
324
|
+
const manager = new NativeSessionManager(fixture.root);
|
|
325
|
+
const launch = {
|
|
326
|
+
leaseId: randomUUID(),
|
|
327
|
+
executable: join(fixture.cwd, 'codex'),
|
|
328
|
+
safetyTier: 'sandboxed',
|
|
329
|
+
environment: { set: {}, unset: [] },
|
|
330
|
+
};
|
|
331
|
+
const params = { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd };
|
|
332
|
+
const session = await manager.ensure('owner', params, launch);
|
|
333
|
+
const next = { ...launch, leaseId: randomUUID() };
|
|
334
|
+
const fault = t.mock.method(fs, 'fsyncSync', () => {
|
|
335
|
+
throw new Error('Lease flush failed');
|
|
336
|
+
});
|
|
337
|
+
syncBuiltinESMExports();
|
|
338
|
+
try {
|
|
339
|
+
assert.throws(() => manager.transferWorker('owner', session.id, session.generation, launch.leaseId, next), /Lease flush failed/);
|
|
340
|
+
fault.mock.restore();
|
|
341
|
+
syncBuiltinESMExports();
|
|
342
|
+
assert.throws(() => manager.assertWorkerLease('owner', session.id, session.generation, next.leaseId), /Lease flush failed/);
|
|
343
|
+
assert.throws(() => manager.transferWorker('owner', session.id, session.generation, launch.leaseId, next), /Lease flush failed/);
|
|
344
|
+
await assert.rejects(manager.ensure('owner', params, next), /Lease flush failed/);
|
|
345
|
+
manager.assertWorkerLease('owner', session.id, session.generation, next.leaseId, true);
|
|
346
|
+
await manager.close('owner', session.id);
|
|
347
|
+
const reloaded = new NativeSessionManager(fixture.root);
|
|
348
|
+
const closed = await reloaded.ensure('owner', params, next);
|
|
349
|
+
assert.equal(closed.state, 'closed');
|
|
350
|
+
assert.equal(closed.workerLeaseId, next.leaseId);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
fault.mock.restore();
|
|
354
|
+
syncBuiltinESMExports();
|
|
355
|
+
for (const session of manager.list('owner'))
|
|
356
|
+
await manager.close('owner', session.id);
|
|
357
|
+
fixture.restore();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
test('cancelling an initial worker reservation prevents a delayed launch and survives reload', async () => {
|
|
361
|
+
const fixture = setup();
|
|
362
|
+
const manager = new NativeSessionManager(fixture.root);
|
|
363
|
+
const params = { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd };
|
|
364
|
+
const launch = {
|
|
365
|
+
leaseId: randomUUID(),
|
|
366
|
+
executable: join(fixture.cwd, 'codex'),
|
|
367
|
+
safetyTier: 'sandboxed',
|
|
368
|
+
environment: { set: {}, unset: [] },
|
|
369
|
+
};
|
|
370
|
+
const pending = manager.ensure('owner', params, launch);
|
|
371
|
+
try {
|
|
372
|
+
assert.equal((await manager.cancelWorker('owner', params.sessionId, launch.leaseId)).cancelled, true);
|
|
373
|
+
await assert.rejects(pending, /cancelled before launch/);
|
|
374
|
+
assert.equal(existsSync(join(fixture.cwd, 'native-start.json')), false);
|
|
375
|
+
assert.equal(manager.list('owner').length, 0);
|
|
376
|
+
const reloaded = new NativeSessionManager(fixture.root);
|
|
377
|
+
await assert.rejects(reloaded.ensure('owner', params, launch), /cancelled before launch/);
|
|
378
|
+
await assert.rejects(reloaded.cancelWorker('other', params.sessionId, launch.leaseId), /another owner/);
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
await Promise.allSettled([pending]);
|
|
382
|
+
for (const session of manager.list('owner'))
|
|
383
|
+
await manager.close('owner', session.id);
|
|
384
|
+
fixture.restore();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
test('cancelling a reserved handoff fences a delayed transfer and preserves later task leases', async () => {
|
|
388
|
+
const fixture = setup();
|
|
389
|
+
const manager = new NativeSessionManager(fixture.root);
|
|
390
|
+
const params = { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd };
|
|
391
|
+
const launch = {
|
|
392
|
+
leaseId: randomUUID(),
|
|
393
|
+
executable: join(fixture.cwd, 'codex'),
|
|
394
|
+
safetyTier: 'full-auto',
|
|
395
|
+
environment: { set: {}, unset: [] },
|
|
396
|
+
};
|
|
397
|
+
const successor = { ...launch, leaseId: randomUUID() };
|
|
398
|
+
try {
|
|
399
|
+
const session = await manager.ensure('owner', params, launch);
|
|
400
|
+
await assert.rejects(manager.cancelWorker('other', session.id, successor.leaseId, session.generation, launch.leaseId), /ownership changed/);
|
|
401
|
+
assert.equal(manager.read('owner', session.id).session.workerLeaseId, launch.leaseId);
|
|
402
|
+
const cancel = manager.cancelWorker('owner', session.id, successor.leaseId, session.generation, launch.leaseId);
|
|
403
|
+
const delayed = manager.transferWorker('owner', session.id, session.generation, launch.leaseId, successor);
|
|
404
|
+
assert.equal(delayed.workerLeaseId, successor.leaseId);
|
|
405
|
+
const stopped = await cancel;
|
|
406
|
+
assert.equal(stopped.session?.processStopped, true);
|
|
407
|
+
assert.equal(stopped.session?.state, 'closed');
|
|
408
|
+
const retried = await manager.ensure('owner', params, successor);
|
|
409
|
+
assert.equal(retried.state, 'closed');
|
|
410
|
+
assert.equal(retried.processPid, session.processPid);
|
|
411
|
+
assert.equal((await new NativeSessionManager(fixture.root).ensure('owner', params, successor)).state, 'closed');
|
|
412
|
+
const resumed = await manager.create('owner', { ...params, resumeSessionId: session.nativeSessionId }, successor);
|
|
413
|
+
const later = { ...successor, leaseId: randomUUID() };
|
|
414
|
+
manager.transferWorker('owner', session.id, resumed.generation, successor.leaseId, later);
|
|
415
|
+
await assert.rejects(manager.cancelWorker('owner', session.id, successor.leaseId, resumed.generation, launch.leaseId), /ownership changed/);
|
|
416
|
+
assert.equal(manager.read('owner', session.id).session.workerLeaseId, later.leaseId);
|
|
417
|
+
assert.ok(alive(resumed.processPid));
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
for (const session of manager.list('owner'))
|
|
421
|
+
await manager.close('owner', session.id);
|
|
422
|
+
fixture.restore();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
test('cancelled recovery IDs survive reload while a new explicit recovery remains available', async () => {
|
|
426
|
+
const fixture = setup();
|
|
427
|
+
let manager = new NativeSessionManager(fixture.root);
|
|
428
|
+
const launch = {
|
|
429
|
+
leaseId: randomUUID(),
|
|
430
|
+
executable: join(fixture.cwd, 'codex'),
|
|
431
|
+
safetyTier: 'sandboxed',
|
|
432
|
+
environment: { set: {}, unset: [] },
|
|
433
|
+
};
|
|
434
|
+
try {
|
|
435
|
+
const first = await manager.ensure('owner', { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd }, launch);
|
|
436
|
+
await manager.closeWorker('owner', first.id, first.generation, launch.leaseId);
|
|
437
|
+
const params = {
|
|
438
|
+
sessionId: first.id,
|
|
439
|
+
generation: first.generation,
|
|
440
|
+
runner: 'codex',
|
|
441
|
+
cwd: fixture.cwd,
|
|
442
|
+
resumeSessionId: first.nativeSessionId,
|
|
443
|
+
};
|
|
444
|
+
const cancelledIds = [randomUUID(), randomUUID()];
|
|
445
|
+
for (const commandId of cancelledIds)
|
|
446
|
+
await manager.cancelWorker('owner', first.id, launch.leaseId, first.generation, undefined, commandId);
|
|
447
|
+
manager = new NativeSessionManager(fixture.root);
|
|
448
|
+
for (const commandId of cancelledIds)
|
|
449
|
+
await assert.rejects(manager.resumeWorker('owner', { ...params, commandId }, launch), /operation was cancelled/);
|
|
450
|
+
const resumed = await manager.resumeWorker('owner', { ...params, commandId: randomUUID() }, launch);
|
|
451
|
+
assert.notEqual(resumed.generation, first.generation);
|
|
452
|
+
for (const commandId of cancelledIds) {
|
|
453
|
+
await manager.cancelWorker('owner', first.id, launch.leaseId, first.generation, undefined, commandId);
|
|
454
|
+
assert.equal(manager.read('owner', first.id).session.generation, resumed.generation);
|
|
455
|
+
assert.ok(alive(resumed.processPid));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
for (const session of manager.list('owner'))
|
|
460
|
+
await manager.close('owner', session.id);
|
|
461
|
+
fixture.restore();
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
for (const race of ['cancel', 'transfer']) {
|
|
465
|
+
test(`worker resume rechecks ${race} after its asynchronous executable probe`, async () => {
|
|
466
|
+
const fixture = setup();
|
|
467
|
+
const manager = new NativeSessionManager(fixture.root);
|
|
468
|
+
const launch = {
|
|
469
|
+
leaseId: randomUUID(),
|
|
470
|
+
executable: join(fixture.cwd, 'codex'),
|
|
471
|
+
safetyTier: 'sandboxed',
|
|
472
|
+
environment: { set: {}, unset: [] },
|
|
473
|
+
};
|
|
474
|
+
let pending;
|
|
475
|
+
try {
|
|
476
|
+
const first = await manager.ensure('owner', { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd }, launch);
|
|
477
|
+
await manager.closeWorker('owner', first.id, first.generation, launch.leaseId);
|
|
478
|
+
const startReceipt = readFileSync(join(fixture.cwd, 'native-start.json'), 'utf8');
|
|
479
|
+
const binary = readFileSync(launch.executable, 'utf8');
|
|
480
|
+
writeFileSync(launch.executable, binary.replace("if (process.argv.includes('--version')) {", `if (process.argv.includes('--version')) {
|
|
481
|
+
const probeFs = require('node:fs');
|
|
482
|
+
probeFs.writeFileSync('version-entered', '');
|
|
483
|
+
while (!probeFs.existsSync('version-release')) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
484
|
+
`));
|
|
485
|
+
const commandId = randomUUID();
|
|
486
|
+
pending = manager.resumeWorker('owner', {
|
|
487
|
+
sessionId: first.id,
|
|
488
|
+
generation: first.generation,
|
|
489
|
+
runner: 'codex',
|
|
490
|
+
cwd: fixture.cwd,
|
|
491
|
+
resumeSessionId: first.nativeSessionId,
|
|
492
|
+
commandId,
|
|
493
|
+
}, launch);
|
|
494
|
+
const rejected = assert.rejects(pending, race === 'cancel' ? /operation was cancelled/ : /lease changed/);
|
|
495
|
+
await until(() => existsSync(join(fixture.cwd, 'version-entered')));
|
|
496
|
+
if (race === 'cancel')
|
|
497
|
+
await manager.cancelWorker('owner', first.id, launch.leaseId, first.generation, undefined, commandId);
|
|
498
|
+
else
|
|
499
|
+
manager.transferWorker('owner', first.id, first.generation, launch.leaseId, {
|
|
500
|
+
...launch,
|
|
501
|
+
leaseId: randomUUID(),
|
|
502
|
+
});
|
|
503
|
+
writeFileSync(join(fixture.cwd, 'version-release'), '');
|
|
504
|
+
await rejected;
|
|
505
|
+
const after = manager.read('owner', first.id).session;
|
|
506
|
+
assert.equal(after.generation, first.generation);
|
|
507
|
+
assert.equal(after.processStopped, true);
|
|
508
|
+
assert.equal(readFileSync(join(fixture.cwd, 'native-start.json'), 'utf8'), startReceipt);
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
writeFileSync(join(fixture.cwd, 'version-release'), '');
|
|
512
|
+
if (pending)
|
|
513
|
+
await Promise.allSettled([pending]);
|
|
514
|
+
for (const session of manager.list('owner'))
|
|
515
|
+
await manager.close('owner', session.id);
|
|
516
|
+
fixture.restore();
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
test('replayed worker cancellation cannot stop an explicitly resumed generation', async () => {
|
|
521
|
+
const fixture = setup();
|
|
522
|
+
const manager = new NativeSessionManager(fixture.root);
|
|
523
|
+
const params = { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd };
|
|
524
|
+
const launch = {
|
|
525
|
+
leaseId: randomUUID(),
|
|
526
|
+
executable: join(fixture.cwd, 'codex'),
|
|
527
|
+
safetyTier: 'sandboxed',
|
|
528
|
+
environment: { set: {}, unset: [] },
|
|
529
|
+
};
|
|
530
|
+
try {
|
|
531
|
+
const first = await manager.ensure('owner', params, launch);
|
|
532
|
+
await manager.cancelWorker('owner', first.id, launch.leaseId);
|
|
533
|
+
const resumed = await manager.create('owner', { ...params, resumeSessionId: first.nativeSessionId }, launch);
|
|
534
|
+
assert.notEqual(resumed.generation, first.generation);
|
|
535
|
+
for (const generation of [undefined, first.generation]) {
|
|
536
|
+
const replay = await manager.cancelWorker('owner', first.id, launch.leaseId, generation);
|
|
537
|
+
assert.equal(replay.generation, first.generation);
|
|
538
|
+
assert.equal(manager.read('owner', first.id).session.state, 'idle');
|
|
539
|
+
assert.ok(alive(resumed.processPid));
|
|
540
|
+
}
|
|
541
|
+
const closed = await manager.cancelWorker('owner', first.id, launch.leaseId, resumed.generation);
|
|
542
|
+
assert.equal(closed.session?.state, 'closed');
|
|
543
|
+
assert.equal(closed.session?.processStopped, true);
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
for (const session of manager.list('owner'))
|
|
547
|
+
await manager.close('owner', session.id);
|
|
548
|
+
fixture.restore();
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
test('worker host rejects public input and accepts only leased commands and permission replies', async () => {
|
|
552
|
+
const fixture = setup();
|
|
553
|
+
const client = new NativeSessionClient(fixture.root);
|
|
554
|
+
const launch = {
|
|
555
|
+
leaseId: randomUUID(),
|
|
556
|
+
executable: join(fixture.cwd, 'codex'),
|
|
557
|
+
safetyTier: 'sandboxed',
|
|
558
|
+
environment: { set: {}, unset: [] },
|
|
559
|
+
};
|
|
560
|
+
try {
|
|
561
|
+
const session = await client.ensureWorker('owner', { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd }, launch);
|
|
562
|
+
const target = {
|
|
563
|
+
sessionId: session.id,
|
|
564
|
+
generation: session.generation,
|
|
565
|
+
leaseId: launch.leaseId,
|
|
566
|
+
};
|
|
567
|
+
await assert.rejects(client.send('owner', session.id, 'unleased', 'approval'), /requires its run context/);
|
|
568
|
+
await assert.rejects(client.interrupt('owner', session.id), /requires its run context/);
|
|
569
|
+
await assert.rejects(client.close('owner', session.id), /requires its run context/);
|
|
570
|
+
await assert.rejects(client.sendWorker('owner', { ...target, leaseId: randomUUID() }, 'stale', 'approval'), /lease changed/);
|
|
571
|
+
assert.equal(existsSync(join(fixture.cwd, 'inputs')), false);
|
|
572
|
+
await client.sendWorker('owner', target, 'leased-approval', 'approval');
|
|
573
|
+
await until(async () => (await client.read('owner', session.id)).pendingRequests.length === 1);
|
|
574
|
+
const pending = (await client.read('owner', session.id)).pendingRequests[0].request.id;
|
|
575
|
+
await assert.rejects(client.respond('owner', session.id, pending, { decision: 'approve' }), /requires its run context/);
|
|
576
|
+
await assert.rejects(client.respondWorker('owner', { ...target, generation: randomUUID() }, pending, {
|
|
577
|
+
decision: 'approve',
|
|
578
|
+
}), /generation.*changed/);
|
|
579
|
+
assert.equal((await client.read('owner', session.id)).pendingRequests.length, 1);
|
|
580
|
+
await client.respondWorker('owner', target, pending, { decision: 'deny' });
|
|
581
|
+
await until(async () => (await client.read('owner', session.id)).session.state === 'idle');
|
|
582
|
+
assert.equal((await client.closeWorker('owner', target)).processStopped, true);
|
|
583
|
+
}
|
|
584
|
+
finally {
|
|
585
|
+
const file = join(fixture.root, 'host.json');
|
|
586
|
+
if (existsSync(file)) {
|
|
587
|
+
const host = readJson(file);
|
|
588
|
+
if (alive(host.pid)) {
|
|
589
|
+
process.kill(host.pid, 'SIGTERM');
|
|
590
|
+
await until(() => !alive(host.pid));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
rmSync(socketDirectory(fixture.root), { recursive: true, force: true });
|
|
594
|
+
fixture.restore();
|
|
595
|
+
}
|
|
596
|
+
});
|
|
175
597
|
test('old native hosts retain ordinary reads but refuse ensured creation before IPC', async () => {
|
|
176
598
|
const fixture = setup();
|
|
177
599
|
const socketRoot = socketDirectory(fixture.root);
|
|
@@ -198,6 +620,12 @@ test('old native hosts retain ordinary reads but refuse ensured creation before
|
|
|
198
620
|
assert.deepEqual(await client.list('owner'), []);
|
|
199
621
|
await assert.rejects(client.ensure('owner', { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd }), /host upgrade required/);
|
|
200
622
|
assert.deepEqual(requests, ['list']);
|
|
623
|
+
await assert.rejects(client.ensureWorker('owner', { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd }, {
|
|
624
|
+
leaseId: randomUUID(),
|
|
625
|
+
safetyTier: 'sandboxed',
|
|
626
|
+
environment: { set: {}, unset: [] },
|
|
627
|
+
}), /host upgrade required for worker launch/);
|
|
628
|
+
assert.deepEqual(requests, ['list']);
|
|
201
629
|
}
|
|
202
630
|
finally {
|
|
203
631
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
@@ -513,4 +941,36 @@ test('unregistered host cannot bind and exits if its supervisor disappears', asy
|
|
|
513
941
|
rmSync(root, { recursive: true, force: true });
|
|
514
942
|
}
|
|
515
943
|
});
|
|
944
|
+
test('a stopped worker transfers its lease before resuming the saved conversation', async () => {
|
|
945
|
+
const fixture = setup();
|
|
946
|
+
const manager = new NativeSessionManager(fixture.root);
|
|
947
|
+
const params = { sessionId: randomUUID(), runner: 'codex', cwd: fixture.cwd };
|
|
948
|
+
const launch = {
|
|
949
|
+
leaseId: randomUUID(),
|
|
950
|
+
executable: join(fixture.cwd, 'codex'),
|
|
951
|
+
safetyTier: 'full-auto',
|
|
952
|
+
environment: { set: {}, unset: [] },
|
|
953
|
+
};
|
|
954
|
+
const successor = { ...launch, leaseId: randomUUID() };
|
|
955
|
+
try {
|
|
956
|
+
const first = await manager.ensure('owner', params, launch);
|
|
957
|
+
await manager.cancelWorker('owner', first.id, launch.leaseId, first.generation);
|
|
958
|
+
const transferred = manager.transferWorker('owner', first.id, first.generation, launch.leaseId, successor);
|
|
959
|
+
assert.equal(transferred.processStopped, true);
|
|
960
|
+
assert.equal(transferred.generation, first.generation);
|
|
961
|
+
assert.equal(transferred.workerLeaseId, successor.leaseId);
|
|
962
|
+
assert.throws(() => manager.assertWorkerLease('owner', first.id, first.generation, launch.leaseId), /lease changed/);
|
|
963
|
+
const resumed = await manager.create('owner', { ...params, resumeSessionId: first.nativeSessionId }, successor);
|
|
964
|
+
assert.equal(resumed.nativeSessionId, first.nativeSessionId);
|
|
965
|
+
assert.notEqual(resumed.generation, first.generation);
|
|
966
|
+
await manager.cancelWorker('owner', first.id, launch.leaseId, first.generation);
|
|
967
|
+
assert.ok(alive(resumed.processPid));
|
|
968
|
+
assert.equal(manager.read('owner', first.id).session.workerLeaseId, successor.leaseId);
|
|
969
|
+
}
|
|
970
|
+
finally {
|
|
971
|
+
for (const session of manager.list('owner'))
|
|
972
|
+
await manager.close('owner', session.id);
|
|
973
|
+
fixture.restore();
|
|
974
|
+
}
|
|
975
|
+
});
|
|
516
976
|
//# sourceMappingURL=durability.test.js.map
|