@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,287 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createOpenCodeToolBridge } from "../src/server/conversation/openCodeTools.js";
|
|
7
|
+
import conversationInstructions from "../src/server/conversation/openCodePlugin.js";
|
|
8
|
+
import { createOpenCodeSharedRuntime } from "../src/server/conversation/openCodeRuntime.js";
|
|
9
|
+
import { openCodeConversationAgents } from "../src/server/conversation/openCodeProcess.js";
|
|
10
|
+
|
|
11
|
+
test("native OpenCode tools and shell environment require the bound conversation's host grant", async t => {
|
|
12
|
+
const directory = await mkdtemp(path.join(tmpdir(), "jskit-native-tools-"));
|
|
13
|
+
const file = path.join(directory, "instructions.json");
|
|
14
|
+
const names = ["JSKIT_OPENCODE_ENV_REGISTRY"];
|
|
15
|
+
const previous = names.map(name => process.env[name]);
|
|
16
|
+
t.after(async () => {
|
|
17
|
+
names.forEach((name, index) => { if (previous[index] === undefined) delete process.env[name]; else process.env[name] = previous[index]; });
|
|
18
|
+
await rm(directory, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
process.env.JSKIT_OPENCODE_ENV_REGISTRY = file;
|
|
21
|
+
const binding = { upstreamSessionId: "owned", env: { PROJECT_VALUE: "application-owned" },
|
|
22
|
+
conversation: { nativeTools: true, systemPrompt: "Current instructions" } };
|
|
23
|
+
await writeFile(file, JSON.stringify({ sessions: [binding] }));
|
|
24
|
+
const plugin = await conversationInstructions();
|
|
25
|
+
await plugin["tool.execute.before"]({ sessionID: "owned", tool: "bash" });
|
|
26
|
+
const output = { env: {} };
|
|
27
|
+
await plugin["shell.env"]({ sessionID: "owned" }, output);
|
|
28
|
+
assert.deepEqual(output.env, { PROJECT_VALUE: "application-owned" });
|
|
29
|
+
await assert.rejects(plugin["tool.execute.before"]({ sessionID: "foreign", tool: "bash" }), /no instruction owner/);
|
|
30
|
+
await assert.rejects(plugin["shell.env"]({ sessionID: "foreign" }, { env: {} }), /no instruction owner/);
|
|
31
|
+
binding.conversation.nativeTools = false;
|
|
32
|
+
await writeFile(file, JSON.stringify({ sessions: [binding] }));
|
|
33
|
+
await assert.rejects(plugin["tool.execute.before"]({ sessionID: "owned", tool: "bash" }), /no authorization/);
|
|
34
|
+
const revoked = { env: {} };
|
|
35
|
+
await plugin["shell.env"]({ sessionID: "owned" }, revoked);
|
|
36
|
+
assert.deepEqual(revoked.env, {});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("the native tool connection requires its private credential and bounds incoming arguments", async t => {
|
|
40
|
+
const calls = [];
|
|
41
|
+
const bridge = await createOpenCodeToolBridge({ schemas: [], maxArgumentBytes: 16,
|
|
42
|
+
execute: async value => { calls.push(value); return { ok: true }; } });
|
|
43
|
+
t.after(() => bridge.close());
|
|
44
|
+
const { url, token } = bridge.configuration;
|
|
45
|
+
const request = { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: '{}' };
|
|
46
|
+
assert.equal((await fetch(url, { ...request, headers: { "content-type": "application/json" } })).status, 403);
|
|
47
|
+
assert.equal((await fetch(url, { ...request, headers: { ...request.headers, origin: "https://another.invalid" } })).status, 403);
|
|
48
|
+
assert.equal((await fetch(url, { ...request, body: "{" })).status, 400);
|
|
49
|
+
const tooLarge = await fetch(url, { ...request, body: '"' + "x".repeat(5000) + '"' });
|
|
50
|
+
assert.equal(tooLarge.status, 400);
|
|
51
|
+
assert.match((await tooLarge.json()).error, /size limit/);
|
|
52
|
+
assert.equal(calls.length, 0);
|
|
53
|
+
assert.deepEqual(await (await fetch(url, request)).json(), { ok: true });
|
|
54
|
+
assert.deepEqual(calls, [{}]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("native plugin definitions retain exact schemas and native identity without enabling built-in tools", async t => {
|
|
58
|
+
const schema = { type: "object", properties: { query: { type: "string" } }, additionalProperties: false };
|
|
59
|
+
const calls = [];
|
|
60
|
+
const bridge = await createOpenCodeToolBridge({ schemas: [{ type: "function", function: {
|
|
61
|
+
name: "assistant_action_search", description: "Search operations", parameters: schema
|
|
62
|
+
} }], maxArgumentBytes: 1024, execute: async value => { calls.push(value); return { ok: true, result: [] }; } });
|
|
63
|
+
t.after(() => bridge.close());
|
|
64
|
+
const directory = await mkdtemp(path.join(tmpdir(), "jskit-opencode-tool-binding-"));
|
|
65
|
+
const file = path.join(directory, "environments.json");
|
|
66
|
+
const names = ["JSKIT_OPENCODE_ENV_REGISTRY", "JSKIT_OPENCODE_TOOL_SCHEMAS"];
|
|
67
|
+
const previous = names.map(name => process.env[name]);
|
|
68
|
+
process.env.JSKIT_OPENCODE_ENV_REGISTRY = file;
|
|
69
|
+
process.env.JSKIT_OPENCODE_TOOL_SCHEMAS = JSON.stringify(bridge.configuration.schemas);
|
|
70
|
+
await writeFile(file, JSON.stringify({ sessions: [{ upstreamSessionId: "session",
|
|
71
|
+
conversation: { tools: bridge.configuration, nativeTools: false } }] }));
|
|
72
|
+
t.after(async () => {
|
|
73
|
+
names.forEach((name, index) => { if (previous[index] === undefined) delete process.env[name]; else process.env[name] = previous[index]; });
|
|
74
|
+
await rm(directory, { recursive: true, force: true });
|
|
75
|
+
});
|
|
76
|
+
const plugin = await conversationInstructions();
|
|
77
|
+
const output = {};
|
|
78
|
+
plugin["tool.definition"]({ toolID: "assistant_action_search" }, output);
|
|
79
|
+
assert.deepEqual(output.jsonSchema, schema);
|
|
80
|
+
await assert.rejects(plugin["tool.execute.before"]({ tool: "bash", sessionID: "session" }), /no authorization/);
|
|
81
|
+
const tool = plugin.tool.assistant_action_search;
|
|
82
|
+
await assert.rejects(tool.execute({}, { sessionID: "session", messageID: "message" }), /tool identity/);
|
|
83
|
+
assert.equal(await tool.execute({}, { sessionID: "session", messageID: "message", callID: "call", abort: new AbortController().signal }),
|
|
84
|
+
'{"ok":true,"result":[]}');
|
|
85
|
+
assert.deepEqual(calls, [{ sessionId: "session", messageId: "message", id: "call", name: "assistant_action_search", input: {} }]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("closing the private connection aborts native work and waits for the invoked operation", async () => {
|
|
89
|
+
const entered = Promise.withResolvers(), complete = Promise.withResolvers();
|
|
90
|
+
let signal;
|
|
91
|
+
const bridge = await createOpenCodeToolBridge({ schemas: [], maxArgumentBytes: 1024,
|
|
92
|
+
async execute(_input, incoming) { signal = incoming; entered.resolve(); return complete.promise; } });
|
|
93
|
+
const { url, token } = bridge.configuration;
|
|
94
|
+
const request = fetch(url, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: '{}' });
|
|
95
|
+
const rejected = assert.rejects(request, /fetch failed/);
|
|
96
|
+
await entered.promise;
|
|
97
|
+
let stopped = false;
|
|
98
|
+
const stopping = bridge.close().then(() => { stopped = true; });
|
|
99
|
+
try {
|
|
100
|
+
assert.equal(signal.aborted, true);
|
|
101
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
102
|
+
assert.equal(stopped, false);
|
|
103
|
+
} finally { complete.resolve({ ok: true }); await stopping; }
|
|
104
|
+
await rejected;
|
|
105
|
+
await assert.rejects(fetch(url), /fetch failed/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("one native plugin routes current instructions, command policy and tools by registered conversation", async t => {
|
|
109
|
+
const directory = await mkdtemp(path.join(tmpdir(), "jskit-opencode-shared-plugin-"));
|
|
110
|
+
const registry = path.join(directory, "environments.json");
|
|
111
|
+
const names = ["JSKIT_OPENCODE_ENV_REGISTRY", "JSKIT_OPENCODE_TOOL_SCHEMAS"];
|
|
112
|
+
const previous = names.map(name => process.env[name]);
|
|
113
|
+
t.after(async () => {
|
|
114
|
+
names.forEach((name, index) => { if (previous[index] === undefined) delete process.env[name]; else process.env[name] = previous[index]; });
|
|
115
|
+
await rm(directory, { recursive: true, force: true });
|
|
116
|
+
});
|
|
117
|
+
const schemas = [{ type: "function", function: { name: "assistant_action_search", description: "Search",
|
|
118
|
+
parameters: { type: "object", properties: { query: { type: "string" } } } } }];
|
|
119
|
+
process.env.JSKIT_OPENCODE_ENV_REGISTRY = registry;
|
|
120
|
+
process.env.JSKIT_OPENCODE_TOOL_SCHEMAS = JSON.stringify(schemas);
|
|
121
|
+
const owner = createOpenCodeSharedRuntime();
|
|
122
|
+
const calls = [];
|
|
123
|
+
const bindings = [];
|
|
124
|
+
for (const id of ["first", "second"]) {
|
|
125
|
+
const bridge = await createOpenCodeToolBridge({ schemas, maxArgumentBytes: 1024,
|
|
126
|
+
execute: async input => { calls.push({ owner: id, ...input }); return { owner: id }; } });
|
|
127
|
+
t.after(() => bridge.close());
|
|
128
|
+
bindings.push({ upstreamSessionId: id, workdir: path.join(directory, id), env: { OWNER: id },
|
|
129
|
+
conversation: { systemPrompt: `${id} instructions`, nativeTools: id === "first",
|
|
130
|
+
commandWrapper: `/host/${id}`, tools: bridge.configuration } });
|
|
131
|
+
}
|
|
132
|
+
const [first, second] = bindings;
|
|
133
|
+
await owner.writeBindings(registry, "first", [first]);
|
|
134
|
+
const plugin = await conversationInstructions({ client: { session: { get: async ({ path: { id } }) => ({
|
|
135
|
+
data: { parentID: id === "child" ? "second" : "" }
|
|
136
|
+
}) } } });
|
|
137
|
+
const firstSystem = { system: ["native default"] };
|
|
138
|
+
await plugin["experimental.chat.system.transform"]({ sessionID: "first" }, firstSystem);
|
|
139
|
+
assert.deepEqual(firstSystem.system, ["first instructions"]);
|
|
140
|
+
await owner.writeBindings(registry, "second", [second]);
|
|
141
|
+
const secondSystem = { system: ["native default"] };
|
|
142
|
+
await plugin["experimental.chat.system.transform"]({ sessionID: "child" }, secondSystem);
|
|
143
|
+
assert.deepEqual(secondSystem.system, ["second instructions"]);
|
|
144
|
+
for (const id of ["first", "second"]) {
|
|
145
|
+
await plugin["tool.execute.before"]({ tool: "assistant_action_search", sessionID: id });
|
|
146
|
+
assert.deepEqual(JSON.parse(await plugin.tool.assistant_action_search.execute({ query: id }, {
|
|
147
|
+
sessionID: id, messageID: `message-${id}`, callID: `call-${id}`, abort: new AbortController().signal
|
|
148
|
+
})), { owner: id });
|
|
149
|
+
}
|
|
150
|
+
assert.deepEqual(calls, ["first", "second"].map(id => ({ owner: id, sessionId: id,
|
|
151
|
+
messageId: `message-${id}`, id: `call-${id}`, name: "assistant_action_search", input: { query: id } })));
|
|
152
|
+
const command = { args: { command: "pwd" } };
|
|
153
|
+
await plugin["tool.execute.before"]({ tool: "bash", sessionID: "first" }, command);
|
|
154
|
+
assert.equal(command.args.command, "'/host/first' 'pwd'");
|
|
155
|
+
await assert.rejects(plugin["tool.execute.before"]({ tool: "bash", sessionID: "second" }, { args: { command: "pwd" } }), /no authorization/);
|
|
156
|
+
const environment = { env: {} };
|
|
157
|
+
await plugin["shell.env"]({ cwd: first.workdir }, environment);
|
|
158
|
+
assert.deepEqual(environment.env, { OWNER: "first" });
|
|
159
|
+
const ungranted = { env: {} };
|
|
160
|
+
await plugin["shell.env"]({ sessionID: "second" }, ungranted);
|
|
161
|
+
assert.deepEqual(ungranted.env, {});
|
|
162
|
+
first.conversation.systemPrompt = "updated first instructions";
|
|
163
|
+
await owner.writeBindings(registry, "first", [first]);
|
|
164
|
+
await plugin["experimental.chat.system.transform"]({ sessionID: "first" }, firstSystem);
|
|
165
|
+
assert.deepEqual(firstSystem.system, ["updated first instructions"]);
|
|
166
|
+
delete second.conversation.tools;
|
|
167
|
+
await owner.writeBindings(registry, "second", [second]);
|
|
168
|
+
await assert.rejects(plugin.tool.assistant_action_search.execute({}, {
|
|
169
|
+
sessionID: "second", messageID: "later", callID: "later", abort: new AbortController().signal
|
|
170
|
+
}), /no authorization/);
|
|
171
|
+
await owner.writeBindings(registry, "first", []);
|
|
172
|
+
await assert.rejects(plugin["experimental.chat.system.transform"]({ sessionID: "first" }, { system: [] }), /no instruction owner/);
|
|
173
|
+
await plugin["experimental.chat.system.transform"]({ sessionID: "second" }, secondSystem);
|
|
174
|
+
assert.deepEqual(secondSystem.system, ["second instructions"]);
|
|
175
|
+
assert.equal(calls.length, 2);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("the shared plugin retains host instruction routing, repeated resolution and compaction invalidation", async t => {
|
|
179
|
+
const directory = await mkdtemp(path.join(tmpdir(), "jskit-opencode-host-instructions-"));
|
|
180
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
181
|
+
const registry = path.join(directory, "environments.json");
|
|
182
|
+
await writeFile(registry, JSON.stringify({ sessions: [{ upstreamSessionId: "shared",
|
|
183
|
+
conversation: { systemPrompt: "Native instructions from the captured registry" } }] }));
|
|
184
|
+
let route = "host", revision = "first", reads = 0, changeDuringSelection = false;
|
|
185
|
+
const resolutions = [];
|
|
186
|
+
const plugin = await conversationInstructions({ registryPath: registry,
|
|
187
|
+
async resolveHostInstructions(id) {
|
|
188
|
+
resolutions.push(id);
|
|
189
|
+
const selectedRoute = route;
|
|
190
|
+
const selectedRevision = revision;
|
|
191
|
+
if (changeDuringSelection) {
|
|
192
|
+
changeDuringSelection = false;
|
|
193
|
+
route = "native";
|
|
194
|
+
revision = "changed between reads";
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
conversation: selectedRoute === "native" ? { systemPrompt: "Outer selection is not the native prompt" } : undefined,
|
|
198
|
+
instructions: id === "unknown" ? null : { identity: selectedRevision, placement: "append",
|
|
199
|
+
read: () => { reads++; return `Host ${selectedRevision}`; } }
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
const output = { system: ["Native default"] };
|
|
204
|
+
const system = output.system;
|
|
205
|
+
await plugin["experimental.chat.system.transform"]({ sessionID: "shared" }, output);
|
|
206
|
+
await plugin["experimental.chat.system.transform"]({ sessionID: "shared" }, output);
|
|
207
|
+
assert.deepEqual(resolutions, ["shared", "shared", "shared", "shared"]);
|
|
208
|
+
assert.equal(reads, 1);
|
|
209
|
+
assert.equal(output.system, system);
|
|
210
|
+
assert.deepEqual(output.system, ["Native default", "Host first"]);
|
|
211
|
+
plugin.event({ event: { type: "session.compacted", properties: { sessionID: "shared" } } });
|
|
212
|
+
await plugin["experimental.chat.system.transform"]({ sessionID: "shared" }, output);
|
|
213
|
+
assert.equal(reads, 2);
|
|
214
|
+
assert.deepEqual(output.system, ["Native default", "Host first"]);
|
|
215
|
+
|
|
216
|
+
resolutions.length = 0;
|
|
217
|
+
route = "native";
|
|
218
|
+
await plugin["experimental.chat.system.transform"]({ sessionID: "shared" }, output);
|
|
219
|
+
assert.deepEqual(resolutions, ["shared"]);
|
|
220
|
+
assert.deepEqual(output.system, ["Native instructions from the captured registry"]);
|
|
221
|
+
assert.equal(reads, 2);
|
|
222
|
+
|
|
223
|
+
resolutions.length = 0;
|
|
224
|
+
route = "host";
|
|
225
|
+
changeDuringSelection = true;
|
|
226
|
+
await plugin["experimental.chat.system.transform"]({ sessionID: "shared" }, output);
|
|
227
|
+
assert.deepEqual(resolutions, ["shared", "shared"]);
|
|
228
|
+
assert.deepEqual(output.system, ["Native instructions from the captured registry", "Host changed between reads"]);
|
|
229
|
+
assert.equal(reads, 3);
|
|
230
|
+
route = "host";
|
|
231
|
+
const unknown = { system: ["Unowned native guidance"] };
|
|
232
|
+
await plugin["experimental.chat.system.transform"]({ sessionID: "unknown" }, unknown);
|
|
233
|
+
assert.deepEqual(unknown.system, ["Unowned native guidance"]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Original non-project agent policy and denial cases from Vibe64's server/plugin
|
|
237
|
+
// tests. Common bindings replace promptContext; assertion semantics are retained.
|
|
238
|
+
test("non-project common OpenCode tools stay behind approval and require the execution guard", () => {
|
|
239
|
+
const unguarded = openCodeConversationAgents();
|
|
240
|
+
assert.equal(unguarded["jskit-assistant"].permission, "deny");
|
|
241
|
+
assert.equal(unguarded["jskit-assistant-actions"].permission["*"], "deny");
|
|
242
|
+
const guarded = openCodeConversationAgents({ sessionEnvironmentRegistry: "/private/sessions.json" });
|
|
243
|
+
assert.equal(guarded["jskit-assistant"].permission["*"], "ask");
|
|
244
|
+
assert.equal(guarded["jskit-assistant-actions"].permission["*"], "ask");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("common non-project OpenCode calls retain the original fail-closed execution guard", async t => {
|
|
248
|
+
const directory = await mkdtemp(path.join(tmpdir(), "jskit-opencode-original-guard-"));
|
|
249
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
250
|
+
const registryPath = path.join(directory, "sessions.json");
|
|
251
|
+
const sessions = [{ upstreamSessionId: "private-conversation", conversation: {
|
|
252
|
+
systemPrompt: "You have no tools or shell. Trusted host snapshot: active.", nativeTools: false
|
|
253
|
+
} }];
|
|
254
|
+
await writeFile(registryPath, JSON.stringify({ sessions }));
|
|
255
|
+
const plugin = await conversationInstructions({ registryPath });
|
|
256
|
+
for (const tool of ["bash", "shell", "read", "glob", "grep", "edit", "write", "apply_patch", "task", "webfetch", "websearch", "skill", "todowrite", "question", "future-tool"]) {
|
|
257
|
+
const output = { args: { command: "ls -la /private/conversation", subagent_type: "general" } };
|
|
258
|
+
const before = structuredClone(output);
|
|
259
|
+
await assert.rejects(
|
|
260
|
+
plugin["tool.execute.before"]({ sessionID: "private-conversation", tool }, output),
|
|
261
|
+
/no authorization/u
|
|
262
|
+
);
|
|
263
|
+
assert.deepEqual(output, before);
|
|
264
|
+
}
|
|
265
|
+
const childPlugin = await conversationInstructions({ registryPath, client: { session: {
|
|
266
|
+
async get() { return { data: { parentID: "private-conversation" } }; }
|
|
267
|
+
} } });
|
|
268
|
+
await assert.rejects(
|
|
269
|
+
childPlugin["tool.execute.before"]({ sessionID: "native-child", tool: "read" }, {}),
|
|
270
|
+
/no authorization/u
|
|
271
|
+
);
|
|
272
|
+
for (const sessionID of ["unregistered-conversation", ""]) {
|
|
273
|
+
await assert.rejects(
|
|
274
|
+
plugin["tool.execute.before"]({ sessionID, tool: "read" }, {}),
|
|
275
|
+
/no instruction owner/u
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const absent = await conversationInstructions({ registryPath: "" });
|
|
279
|
+
await assert.rejects(absent["tool.execute.before"]({ sessionID: "private-conversation", tool: "read" }, {}), /no instruction owner/u);
|
|
280
|
+
const missing = await conversationInstructions({ registryPath: path.join(directory, "missing.json") });
|
|
281
|
+
await assert.rejects(missing["tool.execute.before"]({ sessionID: "private-conversation", tool: "read" }, {}), { code: "ENOENT" });
|
|
282
|
+
delete sessions[0].conversation;
|
|
283
|
+
await writeFile(registryPath, JSON.stringify({ sessions }));
|
|
284
|
+
await assert.rejects(plugin["tool.execute.before"]({ sessionID: "private-conversation", tool: "read" }, {}), /no authorization/u);
|
|
285
|
+
await writeFile(registryPath, "unreadable registry");
|
|
286
|
+
await assert.rejects(plugin["tool.execute.before"]({ sessionID: "private-conversation", tool: "read" }, {}), SyntaxError);
|
|
287
|
+
});
|