@jskit-ai/assistant-core 0.1.236 → 0.1.238
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 +4 -1
- package/examples/conversation/integrations.json +6 -4
- package/examples/conversation/package.json +3 -3
- package/package.json +1 -1
- package/src/client/conversation/messageDelivery.js +13 -3
- package/src/server/conversation/claudeTurn.js +32 -60
- package/src/server/conversation/codexProcess.js +8 -3
- package/src/server/conversation/localExecution.js +120 -43
- package/src/server/conversation/openCodePlugin.js +3 -2
- package/src/server/conversation/openCodeProcess.js +11 -3
- package/src/server/conversation/openCodeTurn.js +13 -2
- package/test/aiConnectionClient.test.js +2 -2
- package/test/claudeConversationSession.test.js +4 -4
- package/test/claudeTurn.test.js +120 -82
- package/test/codexConversationSession.test.js +2 -2
- package/test/codexProcess.test.js +51 -0
- package/test/conversationReplacement.test.js +1 -1
- package/test/conversationSession.test.js +3 -3
- package/test/localExecution.test.js +150 -0
- package/test/messageDelivery.test.js +17 -0
- package/test/openCodeTools.test.js +10 -0
- package/test/opencodeConversationSession.test.js +28 -3
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import { createConversationProcessIdentity } from "../src/server/conversation/localExecution.js";
|
|
6
|
+
|
|
7
|
+
const TOKEN_ENV = "JSKIT_EXECUTION_RUNTIME_TOKEN";
|
|
8
|
+
const HASH_ENV = "JSKIT_EXECUTION_COMMAND_HASH";
|
|
9
|
+
const runtimeToken = "0b6c8f8e-3f7a-4c1e-9d2b-5a4e6f7a8b9c";
|
|
10
|
+
const commandHash = "abcdef012345";
|
|
11
|
+
// Process-group ids far above any live pid, so liveness probes find nothing.
|
|
12
|
+
const LEADER = 9_000_001;
|
|
13
|
+
const CHILD = 9_000_002;
|
|
14
|
+
const STARTED = "Wed Oct 21 10:00:00 2026";
|
|
15
|
+
const STARTED_SECONDS = String(Date.UTC(2026, 9, 21, 10, 0, 0) / 1000);
|
|
16
|
+
const uid = process.getuid?.() ?? 0;
|
|
17
|
+
|
|
18
|
+
const IDENTITY_ENV = `PATH=/usr/bin ${TOKEN_ENV}=${runtimeToken} ${HASH_ENV}=${commandHash}`;
|
|
19
|
+
|
|
20
|
+
function row(pid, pgid, { owner = uid, stat = "Ss", started = STARTED, args = "/usr/bin/codex app-server", env = IDENTITY_ENV } = {}) {
|
|
21
|
+
return { pid, pgid, owner, stat, started, args, env };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Renders the three darwin listings: every process, then the user's commands without and with `-E`. */
|
|
25
|
+
function psListing(rows, args) {
|
|
26
|
+
if (args[0] === "-ax") {
|
|
27
|
+
return rows.map(({ pid, pgid, owner, stat, started }) =>
|
|
28
|
+
`${String(pid).padStart(7)} ${String(pgid).padStart(7)} ${String(owner).padStart(5)} ${stat.padEnd(4)} ${started}`).join("\n");
|
|
29
|
+
}
|
|
30
|
+
const withEnvironment = args[0] === "-E";
|
|
31
|
+
return rows.filter(({ owner }) => owner === uid).map(({ pid, args: command, env }) =>
|
|
32
|
+
`${String(pid).padStart(7)} ${command}${withEnvironment && env ? ` ${env}` : ""}`).join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function darwinIdentity(table, captureOutput = `${LEADER} ${STARTED}`) {
|
|
36
|
+
const calls = [];
|
|
37
|
+
const identity = createConversationProcessIdentity({
|
|
38
|
+
platform: "darwin",
|
|
39
|
+
async ps(args) {
|
|
40
|
+
calls.push(args);
|
|
41
|
+
if (args.includes("-p")) return captureOutput;
|
|
42
|
+
if (table instanceof Error) throw table;
|
|
43
|
+
return psListing(table, args);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return { calls, identity };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const metadata = (overrides = {}) => ({
|
|
50
|
+
pid: LEADER,
|
|
51
|
+
processState: "running",
|
|
52
|
+
processIdentity: { commandHash, platform: "darwin", runtimeToken, startTimeTicks: STARTED_SECONDS, version: 1, ...overrides }
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("darwin capture records the group leader's ps start time as epoch seconds", async () => {
|
|
56
|
+
const { calls, identity } = darwinIdentity([]);
|
|
57
|
+
const captured = await identity.capture({ commandHash, pid: LEADER, runtimeToken });
|
|
58
|
+
assert.deepEqual(captured, { commandHash, platform: "darwin", runtimeToken, startTimeTicks: STARTED_SECONDS, version: 1 });
|
|
59
|
+
assert.deepEqual(calls, [["-o", "pgid=,lstart=", "-p", String(LEADER)]]);
|
|
60
|
+
assert.equal(identity.isValid(captured), true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("darwin capture parses the padded BSD ps `pgid= lstart=` line", async () => {
|
|
64
|
+
// BSD ps prints lstart with strftime("%c"), which space-pads single-digit days in the C locale.
|
|
65
|
+
const { identity } = darwinIdentity([], ` ${LEADER} Tue Oct 7 09:05:03 2026\n`);
|
|
66
|
+
const captured = await identity.capture({ commandHash, pid: LEADER, runtimeToken });
|
|
67
|
+
assert.equal(captured.startTimeTicks, String(Date.UTC(2026, 9, 7, 9, 5, 3) / 1000));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("darwin capture refuses a non-leader, an unparsable line or a failed ps", async (t) => {
|
|
71
|
+
for (const [name, output] of [
|
|
72
|
+
["another group", `${CHILD} ${STARTED}`],
|
|
73
|
+
["a non-numeric group", `pgid ${STARTED}`],
|
|
74
|
+
["an unknown start format", `${LEADER} 2026-10-21T10:00:00Z`],
|
|
75
|
+
["empty output", ""]
|
|
76
|
+
]) {
|
|
77
|
+
await t.test(name, async () => {
|
|
78
|
+
const { identity } = darwinIdentity([], output);
|
|
79
|
+
await assert.rejects(identity.capture({ commandHash, pid: LEADER, runtimeToken }),
|
|
80
|
+
{ code: "native_process_identity_unavailable" });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("darwin inspection is exact only when every group member carries the identity", async () => {
|
|
86
|
+
const { calls, identity } = darwinIdentity([
|
|
87
|
+
row(LEADER, LEADER),
|
|
88
|
+
row(CHILD, CHILD),
|
|
89
|
+
row(9_000_003, 9_000_003, { env: "" })
|
|
90
|
+
]);
|
|
91
|
+
assert.deepEqual(await identity.inspect(metadata()), { processGroupIds: [LEADER, CHILD], status: "exact" });
|
|
92
|
+
assert.deepEqual(calls, [
|
|
93
|
+
["-ax", "-o", "pid=,pgid=,uid=,stat=,lstart="],
|
|
94
|
+
["-x", "-U", String(uid), "-ww", "-o", "pid=,command="],
|
|
95
|
+
["-E", "-x", "-U", String(uid), "-ww", "-o", "pid=,command="]
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("darwin inspection keeps the absent, mismatch and ambiguous outcomes", async (t) => {
|
|
100
|
+
const cases = [
|
|
101
|
+
["no process in the group", [], {}, "absent"],
|
|
102
|
+
["zombies are ignored", [row(LEADER, LEADER, { stat: "Z" })], {}, "absent"],
|
|
103
|
+
["an unrelated process reuses the group", [row(LEADER, LEADER, { env: "PATH=/usr/bin" })], {}, "mismatch"],
|
|
104
|
+
["the token only in the arguments", [row(LEADER, LEADER, {
|
|
105
|
+
args: `/usr/bin/env -i ${TOKEN_ENV}=${runtimeToken} ${HASH_ENV}=${commandHash} codex`, env: "PATH=/usr/bin"
|
|
106
|
+
})], {}, "mismatch"],
|
|
107
|
+
["a hidden environment", [row(LEADER, LEADER, { env: "" })], {}, "ambiguous"],
|
|
108
|
+
["the token only in the arguments of a hidden environment", [row(LEADER, LEADER, {
|
|
109
|
+
args: `/usr/bin/env ${TOKEN_ENV}=${runtimeToken} ${HASH_ENV}=${commandHash} codex`, env: ""
|
|
110
|
+
})], {}, "ambiguous"],
|
|
111
|
+
["another runtime token", [row(LEADER, LEADER)], { runtimeToken: randomUUID() }, "mismatch"],
|
|
112
|
+
["the leader restarted", [row(LEADER, LEADER, { started: "Wed Oct 21 10:00:01 2026" })], {}, "ambiguous"],
|
|
113
|
+
["another user's process shares the group", [row(LEADER, LEADER), row(CHILD, LEADER, { owner: uid + 1 })], {}, "ambiguous"],
|
|
114
|
+
["another user's environment is not trusted", [row(LEADER, LEADER, { owner: uid + 1 })], {}, "ambiguous"],
|
|
115
|
+
["an unmarked member shares the group", [row(LEADER, LEADER), row(CHILD, LEADER, { env: "PATH=/usr/bin" })], {}, "ambiguous"],
|
|
116
|
+
["ps fails", new Error("ps unavailable"), {}, "ambiguous"],
|
|
117
|
+
["a Linux identity", [row(LEADER, LEADER)], { platform: "linux-proc" }, "ambiguous"]
|
|
118
|
+
];
|
|
119
|
+
for (const [name, table, overrides, status] of cases) {
|
|
120
|
+
await t.test(name, async () => {
|
|
121
|
+
const { identity } = darwinIdentity(table);
|
|
122
|
+
assert.equal((await identity.inspect(metadata(overrides))).status, status);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
async function liveGroup(t, env) {
|
|
128
|
+
const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], {
|
|
129
|
+
detached: true, stdio: "ignore", env: { ...process.env, ...env }
|
|
130
|
+
});
|
|
131
|
+
await new Promise((resolve, reject) => { child.once("spawn", resolve); child.once("error", reject); });
|
|
132
|
+
t.after(() => { try { process.kill(-child.pid, "SIGKILL"); } catch {} });
|
|
133
|
+
return child;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const nativePlatform = { linux: "linux-proc", darwin: "darwin" }[process.platform];
|
|
137
|
+
|
|
138
|
+
test("the host platform captures, inspects and stops a real owned process group", { skip: !nativePlatform }, async (t) => {
|
|
139
|
+
const token = randomUUID();
|
|
140
|
+
const child = await liveGroup(t, { [TOKEN_ENV]: token, [HASH_ENV]: commandHash });
|
|
141
|
+
const identity = createConversationProcessIdentity();
|
|
142
|
+
const captured = await identity.capture({ commandHash, pid: child.pid, runtimeToken: token });
|
|
143
|
+
assert.equal(captured.platform, nativePlatform);
|
|
144
|
+
const owned = { pid: child.pid, processState: "running", processIdentity: captured };
|
|
145
|
+
assert.deepEqual(await identity.inspect(owned), { processGroupIds: [child.pid], status: "exact" });
|
|
146
|
+
assert.equal((await identity.inspect({ ...owned, processIdentity: { ...captured, runtimeToken: randomUUID() } })).status, "mismatch");
|
|
147
|
+
const stopped = await identity.stop(owned, { termTimeoutMs: 2000 });
|
|
148
|
+
assert.equal(stopped.processExitVerified, true);
|
|
149
|
+
assert.deepEqual(stopped.signaledProcessGroups, [child.pid]);
|
|
150
|
+
});
|
|
@@ -146,6 +146,23 @@ test("a failed message retains its retry identity while later guidance is pendin
|
|
|
146
146
|
assert.equal(delivery.state.sending, false);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
test("a failed client-only message keeps its time while newer saved turns and pending messages follow it", async () => {
|
|
150
|
+
const delivery = createAssistantMessageDelivery({ deliver: async () => ({ ok: false, error: "Unavailable" }) });
|
|
151
|
+
await delivery.send({ message: "Lost" }, { messageId: "lost" });
|
|
152
|
+
const sentAt = delivery.find("lost").createdAtMs;
|
|
153
|
+
const earlier = { turnId: "1", user: { messageId: "earlier", text: "Earlier", at: new Date(sentAt - 1000).toISOString() } };
|
|
154
|
+
const newer = { turnId: "2", user: { messageId: "newer", text: "Newer", at: new Date(sentAt + 1000).toISOString() }, pending: true };
|
|
155
|
+
assert.deepEqual(delivery.turns([earlier, newer]).map(turn => turn.turnId), ["1", "lost", "2"]);
|
|
156
|
+
assert.deepEqual(delivery.turns([earlier]).map(turn => turn.turnId), ["1", "lost"]);
|
|
157
|
+
const waiting = Promise.withResolvers();
|
|
158
|
+
const queued = createAssistantMessageDelivery({ deliver: () => waiting.promise });
|
|
159
|
+
const sending = queued.send({ message: "Queued" }, { messageId: "queued" });
|
|
160
|
+
assert.deepEqual(queued.turns([{ ...newer, user: { ...newer.user, at: new Date(Date.now() + 60000).toISOString() } }])
|
|
161
|
+
.map(turn => turn.turnId), ["2", "queued"]);
|
|
162
|
+
waiting.resolve({ ok: true });
|
|
163
|
+
await sending;
|
|
164
|
+
});
|
|
165
|
+
|
|
149
166
|
test("reset discards queued delivery without waiting for the old transport", async () => {
|
|
150
167
|
const request = Promise.withResolvers();
|
|
151
168
|
const sent = [];
|
|
@@ -242,6 +242,16 @@ test("non-project common OpenCode tools stay behind approval and require the exe
|
|
|
242
242
|
const guarded = openCodeConversationAgents({ sessionEnvironmentRegistry: "/private/sessions.json" });
|
|
243
243
|
assert.equal(guarded["jskit-assistant"].permission["*"], "ask");
|
|
244
244
|
assert.equal(guarded["jskit-assistant-actions"].permission["*"], "ask");
|
|
245
|
+
// OpenCode applies the agent's last matching rule: an agent without application
|
|
246
|
+
// tools must not be offered them, and no agent waits on a repeated-call approval.
|
|
247
|
+
for (const name of ["assistant_action_search", "assistant_action_contract", "assistant_action_execute"]) {
|
|
248
|
+
assert.equal(guarded["jskit-assistant"].permission[name], "deny");
|
|
249
|
+
assert.equal(guarded["jskit-assistant-actions"].permission[name], "allow");
|
|
250
|
+
assert.equal(Object.keys(guarded["jskit-assistant"].permission).indexOf(name) > 0, true);
|
|
251
|
+
}
|
|
252
|
+
assert.equal(guarded["jskit-assistant"].permission.doom_loop, "deny");
|
|
253
|
+
assert.equal(guarded["jskit-assistant-actions"].permission.doom_loop, "deny");
|
|
254
|
+
assert.equal(guarded["jskit-assistant-coding"].permission["*"], "deny");
|
|
245
255
|
});
|
|
246
256
|
|
|
247
257
|
test("common non-project OpenCode calls retain the original fail-closed execution guard", async t => {
|
|
@@ -194,6 +194,18 @@ async function fixture(t, options = {}) {
|
|
|
194
194
|
session.messages.push(row("assistant", randomUUID(), [], { error: { data: { message: error.message } } })); save(); return;
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
if (text === "refused-tools") {
|
|
198
|
+
// A model that keeps retrying an ungranted native tool and never answers.
|
|
199
|
+
while (busy.has(session.id)) {
|
|
200
|
+
const part = { type: "tool", callID: randomUUID(), tool: "assistant_action_search", state: { status: "running", input: {} } };
|
|
201
|
+
session.messages.push(row("assistant", randomUUID(), [part], { parentID: body.messageID, finish: "tool-calls" }));
|
|
202
|
+
try { await hooks["tool.execute.before"]({ tool: part.tool, sessionID: session.id }, { args: {} }); }
|
|
203
|
+
catch (error) { part.state = { ...part.state, status: "error", error: error.message }; }
|
|
204
|
+
save();
|
|
205
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
197
209
|
if (text === "compact") {
|
|
198
210
|
session.messages.push(row("user", randomUUID(), [{ type: "compaction", auto: true }]));
|
|
199
211
|
session.messages.push(row("assistant", randomUUID(), [{ type: "text", text: "Native summary" }], { summary: true, finish: "stop" }));
|
|
@@ -327,7 +339,8 @@ test("OpenCode admits once, streams and saves native reasoning through the commo
|
|
|
327
339
|
assert.equal(trace.find(item => item.url === "/api/session").body.model.id, "test-model");
|
|
328
340
|
assert.equal(trace.find(item => item.url?.endsWith("/prompt_async")).body.model.modelID, "test-model");
|
|
329
341
|
assert.equal(trace[0].inheritedSecret, null);
|
|
330
|
-
assert.deepEqual(trace[0].configuration.agent[trace.find(item => item.url?.endsWith("/prompt_async")).body.agent].permission, { "*": "ask"
|
|
342
|
+
assert.deepEqual(trace[0].configuration.agent[trace.find(item => item.url?.endsWith("/prompt_async")).body.agent].permission, { "*": "ask",
|
|
343
|
+
assistant_action_search: "deny", assistant_action_contract: "deny", assistant_action_execute: "deny", doom_loop: "deny" });
|
|
331
344
|
assert.match(trace[0].configuration.plugin[0], /openCodePlugin\.js$/u);
|
|
332
345
|
assert.deepEqual(trace[0].configuration.provider.test.models["test-model"].limit, { context: 32000, output: 4000 });
|
|
333
346
|
assert.equal(trace[0].configuration.provider.test.models["test-model"].id, "test-model");
|
|
@@ -423,6 +436,17 @@ test("the production recovery stops after two reasoning-only completions through
|
|
|
423
436
|
assert.equal(state.conversationLog[0].assistant, null);
|
|
424
437
|
});
|
|
425
438
|
|
|
439
|
+
test("a model that keeps calling refused native tools fails visibly and stops its native turn", async t => {
|
|
440
|
+
const f = await fixture(t);
|
|
441
|
+
await f.conversation.send({ messageId: "client-message-refused-tools", text: "refused-tools" });
|
|
442
|
+
const state = await f.conversation.wait();
|
|
443
|
+
assert.equal(state.conversationLog.length, 1);
|
|
444
|
+
assert.equal(state.conversationLog[0].metadata.runtime.status, "failed");
|
|
445
|
+
assert.equal(state.error, "The model kept calling tools that this conversation does not provide and gave no answer. No tool ran. Your message is kept; retry or choose another model.");
|
|
446
|
+
assert.equal(state.conversationLog[0].assistant, null);
|
|
447
|
+
assert.ok((await f.trace()).some(row => row.url?.endsWith("/abort")), "The refused native loop is stopped");
|
|
448
|
+
});
|
|
449
|
+
|
|
426
450
|
test("OpenCode steering follows the newly admitted input in the same native conversation", async t => {
|
|
427
451
|
const f = await fixture(t);
|
|
428
452
|
const started = Promise.withResolvers();
|
|
@@ -520,7 +544,7 @@ test("OpenCode shares discovery, saved results and current permissions across na
|
|
|
520
544
|
assert.equal(executions, 1);
|
|
521
545
|
const trace = await f.trace();
|
|
522
546
|
assert.deepEqual(trace[0].configuration.agent[trace.find(item => item.url?.endsWith("/prompt_async")).body.agent].permission, { "*": "ask", assistant_action_search: "allow",
|
|
523
|
-
assistant_action_contract: "allow", assistant_action_execute: "allow" });
|
|
547
|
+
assistant_action_contract: "allow", assistant_action_execute: "allow", doom_loop: "deny" });
|
|
524
548
|
const original = await f.binding();
|
|
525
549
|
await f.first.close();
|
|
526
550
|
const resumed = await f.runtime().open({ id: "conversation", context });
|
|
@@ -1625,7 +1649,8 @@ test("completed OpenCode caller markers preserve ordinary native discovery witho
|
|
|
1625
1649
|
assert.equal(ordinaryPrompt.body.agent, "jskit-assistant-actions");
|
|
1626
1650
|
assert.match(ordinaryPrompt.body.parts[0].text, /\[Application data\]/);
|
|
1627
1651
|
assert.deepEqual(initialTrace[0].configuration.agent[ordinaryPrompt.body.agent].permission, {
|
|
1628
|
-
"*": "ask", assistant_action_search: "allow", assistant_action_contract: "allow", assistant_action_execute: "allow"
|
|
1652
|
+
"*": "ask", assistant_action_search: "allow", assistant_action_contract: "allow", assistant_action_execute: "allow",
|
|
1653
|
+
doom_loop: "deny"
|
|
1629
1654
|
});
|
|
1630
1655
|
await assert.rejects(f.prepare({ messageId: "ordinary-opencode", turnId: receipt.turnId }), /no current verified receipt/);
|
|
1631
1656
|
// The original native fixture invokes discovery only for exact plain tools.
|