@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.
@@ -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.