nolo-cli 0.1.32 → 0.1.34

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.
Files changed (234) hide show
  1. package/README.md +4 -10
  2. package/agent-runtime/agentRecordConfig.ts +10 -0
  3. package/agent-runtime/hostAdapter.ts +10 -1
  4. package/agent-runtime/index.ts +9 -20
  5. package/agent-runtime/localLoop.ts +49 -2
  6. package/agent-runtime/localToolPolicy.ts +76 -34
  7. package/agent-runtime/localWorkspaceTools.ts +30 -333
  8. package/agent-runtime/openAiCompatibleProvider.ts +7 -3
  9. package/agent-runtime/openAiCompatibleProviderConfig.ts +15 -26
  10. package/agent-runtime/platformChatProvider.ts +47 -73
  11. package/agent-runtime/providerResolution.ts +244 -0
  12. package/agent-runtime/runtimeToolPolicy.ts +77 -29
  13. package/agent-runtime/shellCommandPolicy.ts +120 -0
  14. package/agentAliases.ts +14 -83
  15. package/agentCommandSupport.ts +3 -6
  16. package/agentInternalCommandEntries.ts +4 -13
  17. package/agentListCommands.ts +18 -27
  18. package/agentMachineCommands.ts +40 -336
  19. package/agentPullCommand.ts +30 -55
  20. package/agentRecordCommands.ts +84 -1
  21. package/agentRecordHelpers.ts +102 -186
  22. package/agentRunCommand.ts +438 -107
  23. package/agentRuntimeLocal.ts +2 -9
  24. package/ai/agent/cliExecutor.ts +19 -323
  25. package/ai/tools/toolNameAliases.ts +70 -0
  26. package/authCommands.ts +2 -36
  27. package/cli/agentAliases.ts +14 -83
  28. package/cli/agentCommandSupport.ts +3 -6
  29. package/cli/agentInternalCommandEntries.ts +4 -13
  30. package/cli/agentListCommands.ts +18 -27
  31. package/cli/agentMachineCommands.ts +40 -336
  32. package/cli/agentPullCommand.ts +30 -55
  33. package/cli/agentRecordCommands.ts +84 -1
  34. package/cli/agentRecordHelpers.ts +102 -186
  35. package/cli/agentRunCommand.ts +438 -107
  36. package/cli/agentRuntimeLocal.ts +2 -9
  37. package/cli/authCommands.ts +2 -36
  38. package/cli/cliCommandDispatch.ts +2 -2
  39. package/cli/cliEnvHelpers.ts +34 -107
  40. package/cli/client/agentRun.ts +854 -57
  41. package/cli/client/localRuntimeAdapter.ts +59 -159
  42. package/cli/client/streamingOutput.ts +3 -1
  43. package/cli/client/taskRunPrompt.ts +24 -7
  44. package/cli/commandRegistry.ts +4 -5
  45. package/cli/connectorRunArtifact.ts +10 -34
  46. package/cli/connectorWebSocketTarget.ts +6 -10
  47. package/cli/internalCommandEntries.ts +0 -8
  48. package/cli/localRuntimeDb.ts +2 -39
  49. package/cli/machineCommands.ts +189 -190
  50. package/cli/machineDaemonCommands.ts +165 -9
  51. package/cli/machineHeartbeatCommands.ts +2 -1
  52. package/cli/machineStatusCommands.ts +13 -5
  53. package/cli/machineWatchCommands.ts +1 -1
  54. package/cli/machineWsRunDispatch.ts +30 -44
  55. package/cli/machineWsSession.ts +0 -2
  56. package/cli/offlineMarxistsAgentCommand.ts +314 -0
  57. package/cli/runtimeDoctorCommands.ts +72 -15
  58. package/cli/scriptCommandEntries.ts +45 -5
  59. package/cli/tableCommands.ts +133 -33
  60. package/cli/taskRunCommand.ts +211 -16
  61. package/cli/tui/session.ts +372 -43
  62. package/cli/workflowInternalCommandEntries.ts +0 -20
  63. package/cliCommandDispatch.ts +2 -2
  64. package/cliEnvHelpers.ts +34 -107
  65. package/client/agentConfigResolver.test.ts +2 -0
  66. package/client/agentRun.test.ts +12 -208
  67. package/client/agentRun.ts +854 -57
  68. package/client/localProviderResolver.test.ts +26 -2
  69. package/client/localRuntimeAdapter.test.ts +296 -171
  70. package/client/localRuntimeAdapter.ts +59 -159
  71. package/client/localToolPolicy.test.ts +4 -28
  72. package/client/streamingOutput.ts +3 -1
  73. package/client/taskRunPrompt.ts +24 -7
  74. package/commandRegistry.ts +4 -5
  75. package/connectorRunArtifact.ts +10 -34
  76. package/connectorWebSocketTarget.ts +6 -10
  77. package/database/server/serverStoreFactory.ts +2 -8
  78. package/internalCommandEntries.ts +0 -8
  79. package/localRuntimeDb.ts +2 -39
  80. package/machineCommands.ts +189 -190
  81. package/machineDaemonCommands.ts +165 -9
  82. package/machineHeartbeatCommands.ts +2 -1
  83. package/machineStatusCommands.ts +13 -5
  84. package/machineWatchCommands.ts +1 -1
  85. package/machineWsRunDispatch.ts +30 -44
  86. package/machineWsSession.ts +0 -2
  87. package/offlineMarxistsAgentCommand.ts +314 -0
  88. package/package.json +52 -188
  89. package/runtimeDoctorCommands.ts +72 -15
  90. package/scriptCommandEntries.ts +45 -5
  91. package/tableCommands.ts +133 -33
  92. package/taskRunCommand.ts +211 -16
  93. package/tui/session.ts +372 -43
  94. package/workflowInternalCommandEntries.ts +0 -20
  95. package/agent-runtime/dialogStatus.ts +0 -139
  96. package/agent-runtime/hostRuntimeConfig.ts +0 -86
  97. package/agentCreateCustomCommands.ts +0 -278
  98. package/agentDialogsCommands.ts +0 -227
  99. package/agentDoctorCommands.ts +0 -203
  100. package/agentRunLocalWorkspace.ts +0 -182
  101. package/agentRunParsing.ts +0 -298
  102. package/agentSuperviseCommands.ts +0 -8
  103. package/agentUnpublishCommands.ts +0 -113
  104. package/ai/agent/agentRunDialogLineage.ts +0 -56
  105. package/ai/policy/types.ts +0 -64
  106. package/ai/skills/skillSummaryMarker.ts +0 -26
  107. package/app/types.ts +0 -358
  108. package/build.ts +0 -16
  109. package/buildPublish.ts +0 -138
  110. package/buildPublishArtifactAssembly.ts +0 -724
  111. package/buildPublishImportAnalysis.ts +0 -242
  112. package/chat/messages/fetchMessages.ts +0 -84
  113. package/chat/messages/types.ts +0 -185
  114. package/cli/agentCreateCustomCommands.ts +0 -278
  115. package/cli/agentDialogsCommands.ts +0 -227
  116. package/cli/agentDoctorCommands.ts +0 -203
  117. package/cli/agentRunLocalWorkspace.ts +0 -182
  118. package/cli/agentRunParsing.ts +0 -298
  119. package/cli/agentSuperviseCommands.ts +0 -8
  120. package/cli/agentUnpublishCommands.ts +0 -113
  121. package/cli/buildPublish.ts +0 -138
  122. package/cli/buildPublishArtifactAssembly.ts +0 -724
  123. package/cli/buildPublishImportAnalysis.ts +0 -242
  124. package/cli/cliIoTypes.ts +0 -3
  125. package/cli/cliRuntimeDoctorReport.ts +0 -193
  126. package/cli/client/agentRunInput.ts +0 -10
  127. package/cli/client/agentRunLocalExecution.ts +0 -124
  128. package/cli/client/agentRunLocalExecutor.ts +0 -179
  129. package/cli/client/agentRunRuntimeSelection.ts +0 -145
  130. package/cli/client/agentRunServerTransport.ts +0 -397
  131. package/cli/client/agentRunTaskRunRecorder.ts +0 -176
  132. package/cli/customCodingAgentFlow.ts +0 -208
  133. package/cli/customCodingAgentRecord.ts +0 -179
  134. package/cli/dialogDataAccess.ts +0 -186
  135. package/cli/dialogInternalCommandEntries.ts +0 -11
  136. package/cli/dialogReadCommands.ts +0 -180
  137. package/cli/dialogReadHelpers.ts +0 -226
  138. package/cli/dialogStatusCommands.ts +0 -123
  139. package/cli/docInternalCommandEntries.ts +0 -20
  140. package/cli/hybridReadHelpers.ts +0 -109
  141. package/cli/machineCommandPrompts.ts +0 -79
  142. package/cli/machineCommandResolution.ts +0 -43
  143. package/cli/noloReadBridgePrompt.ts +0 -16
  144. package/cli/pageCreateCommands.ts +0 -219
  145. package/cli/pageDeleteCommands.ts +0 -167
  146. package/cli/pageReadCommands.ts +0 -151
  147. package/cli/pageUpdateCommands.ts +0 -249
  148. package/cli/pageWriteHelpers.ts +0 -209
  149. package/cli/publishPackage.ts +0 -36
  150. package/cli/runtimeInternalCommandEntries.ts +0 -15
  151. package/cli/runtimePassthroughCommands.ts +0 -22
  152. package/cli/scriptPassthroughHelpers.ts +0 -28
  153. package/cli/secretScanHelpers.ts +0 -31
  154. package/cli/spaceAttachCommands.ts +0 -134
  155. package/cli/spaceCategoryCommands.ts +0 -263
  156. package/cli/spaceCommands.ts +0 -295
  157. package/cli/spaceDeleteCommands.ts +0 -312
  158. package/cli/spaceInternalCommandEntries.ts +0 -21
  159. package/cli/spaceMutationHelpers.ts +0 -135
  160. package/cli/spaceReadCommands.ts +0 -323
  161. package/cli/tableDataOutput.ts +0 -41
  162. package/cli/tableDataSafety.ts +0 -26
  163. package/cli/tableMetaCommands.ts +0 -307
  164. package/cli/tableMutationCommands.ts +0 -266
  165. package/cli/tableQueryParsing.ts +0 -90
  166. package/cli/tableQueryRequest.ts +0 -53
  167. package/cli/taskRunActions.ts +0 -122
  168. package/cli/taskRunParsing.ts +0 -86
  169. package/cli/taskRunPromptShared.ts +0 -55
  170. package/cli/tui/tuiCatalog.ts +0 -22
  171. package/cli/tui/tuiCommandInterpreter.ts +0 -253
  172. package/cli/tui/tuiPresentation.ts +0 -146
  173. package/cliIoTypes.ts +0 -3
  174. package/cliRuntimeDoctorReport.ts +0 -193
  175. package/client/agentRunInput.ts +0 -10
  176. package/client/agentRunLocalExecution.test.ts +0 -123
  177. package/client/agentRunLocalExecution.ts +0 -124
  178. package/client/agentRunLocalExecutor.ts +0 -179
  179. package/client/agentRunRuntimeSelection.test.ts +0 -159
  180. package/client/agentRunRuntimeSelection.ts +0 -145
  181. package/client/agentRunServerTransport.ts +0 -397
  182. package/client/agentRunTaskRunRecorder.ts +0 -176
  183. package/connector-experimental/daemonLauncher.ts +0 -223
  184. package/create/space/spaceKeys.ts +0 -65
  185. package/create/types.ts +0 -21
  186. package/customCodingAgentFlow.ts +0 -208
  187. package/customCodingAgentRecord.ts +0 -179
  188. package/database/keys.ts +0 -765
  189. package/database/tombstones.ts +0 -52
  190. package/database/utils/ulid.ts +0 -1
  191. package/dialogDataAccess.ts +0 -186
  192. package/dialogInternalCommandEntries.ts +0 -11
  193. package/dialogReadCommands.ts +0 -180
  194. package/dialogReadHelpers.ts +0 -226
  195. package/dialogStatusCommands.ts +0 -123
  196. package/docInternalCommandEntries.ts +0 -20
  197. package/hybridReadHelpers.ts +0 -109
  198. package/machineCommandPrompts.ts +0 -79
  199. package/machineCommandResolution.ts +0 -43
  200. package/noloReadBridgePrompt.ts +0 -16
  201. package/pageCreateCommands.ts +0 -219
  202. package/pageDeleteCommands.ts +0 -167
  203. package/pageReadCommands.ts +0 -151
  204. package/pageUpdateCommands.ts +0 -249
  205. package/pageWriteHelpers.ts +0 -209
  206. package/publishPackage.ts +0 -36
  207. package/runtimeInternalCommandEntries.ts +0 -15
  208. package/runtimePassthroughCommands.ts +0 -22
  209. package/scriptPassthroughHelpers.ts +0 -28
  210. package/secretScanHelpers.ts +0 -31
  211. package/share/helpers.ts +0 -302
  212. package/share/keys.ts +0 -243
  213. package/share/types.ts +0 -100
  214. package/spaceAttachCommands.ts +0 -134
  215. package/spaceCategoryCommands.ts +0 -263
  216. package/spaceCommands.ts +0 -295
  217. package/spaceDeleteCommands.ts +0 -312
  218. package/spaceInternalCommandEntries.ts +0 -21
  219. package/spaceMutationHelpers.ts +0 -135
  220. package/spaceReadCommands.ts +0 -323
  221. package/tableDataOutput.ts +0 -41
  222. package/tableDataSafety.ts +0 -26
  223. package/tableMetaCommands.ts +0 -307
  224. package/tableMutationCommands.ts +0 -266
  225. package/tableQueryParsing.ts +0 -90
  226. package/tableQueryRequest.ts +0 -53
  227. package/taskRunActions.ts +0 -122
  228. package/taskRunParsing.ts +0 -86
  229. package/taskRunPromptShared.ts +0 -55
  230. package/tui/tuiCatalog.ts +0 -22
  231. package/tui/tuiCommandInterpreter.test.ts +0 -169
  232. package/tui/tuiCommandInterpreter.ts +0 -253
  233. package/tui/tuiPresentation.test.ts +0 -154
  234. package/tui/tuiPresentation.ts +0 -146
@@ -11,15 +11,18 @@ describe("CLI local provider resolver", () => {
11
11
  provider: "custom",
12
12
  apiSource: "custom",
13
13
  customProviderUrl: "https://provider.example/v1/chat/completions",
14
+ apiKey: "sk-agent-custom",
15
+ apiKeyHeader: "api-key",
14
16
  },
15
17
  env: {
16
- OPENAI_API_KEY: "sk-env",
18
+ OPENAI_API_KEY: "sk-env-should-not-win",
17
19
  NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
18
20
  },
19
21
  })).toEqual({
20
22
  model: "qwen-coder",
21
23
  endpoint: "https://provider.example/v1/chat/completions",
22
- apiKey: "sk-env",
24
+ apiKey: "sk-agent-custom",
25
+ apiKeyHeader: "api-key",
23
26
  provider: "custom",
24
27
  requestOptions: {},
25
28
  });
@@ -53,6 +56,27 @@ describe("CLI local provider resolver", () => {
53
56
  });
54
57
  });
55
58
 
59
+ test("defaults Xiaomi custom endpoints to api-key auth header", () => {
60
+ expect(resolveCliOpenAiProviderConfig({
61
+ agentConfig: {
62
+ key: "agent-user-1-mimo",
63
+ provider: "custom",
64
+ apiSource: "custom",
65
+ model: "mimo-v2.5-pro",
66
+ customProviderUrl: "https://token-plan-cn.xiaomimimo.com/v1",
67
+ apiKey: "mimo-monthly-key",
68
+ },
69
+ env: {},
70
+ })).toEqual({
71
+ model: "mimo-v2.5-pro",
72
+ endpoint: "https://token-plan-cn.xiaomimimo.com/v1/chat/completions",
73
+ apiKey: "mimo-monthly-key",
74
+ apiKeyHeader: "api-key",
75
+ provider: "custom",
76
+ requestOptions: {},
77
+ });
78
+ });
79
+
56
80
  test("carries explicit agent inference options for request body construction", () => {
57
81
  expect(resolveCliOpenAiProviderConfig({
58
82
  agentConfig: {
@@ -3,7 +3,6 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
 
6
- import { createHostRuntimeConfig } from "../agent-runtime";
7
6
  import { runLocalAgentTurn } from "../agent-runtime/localLoop";
8
7
  import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
9
8
 
@@ -23,9 +22,7 @@ describe("CLI local runtime adapter", () => {
23
22
  "searchFiles",
24
23
  ];
25
24
  const SHELL_LOCAL_CODING_TOOL_NAMES = [
26
- "readFile",
27
- "writeFile",
28
- "editFile",
25
+ ...DEFAULT_LOCAL_CODING_TOOL_NAMES,
29
26
  "execShell",
30
27
  ];
31
28
 
@@ -33,14 +30,6 @@ describe("CLI local runtime adapter", () => {
33
30
  return request?.body?.tools?.map((tool: any) => tool.function.name) ?? [];
34
31
  }
35
32
 
36
- function createCliHostRuntimeConfig(allowShell = false, workspaceBinding: "explicit-cwd" | "default-cwd" = "explicit-cwd") {
37
- return createHostRuntimeConfig({
38
- surface: "cli-local",
39
- workspaceBinding,
40
- allowShell,
41
- });
42
- }
43
-
44
33
  test("loads agent/history from LevelDB and saves dialog/message records back to LevelDB", async () => {
45
34
  const requests: Array<{ url: string; body: any; auth: string | null }> = [];
46
35
  const store = new Map<string, any>([
@@ -289,59 +278,8 @@ describe("CLI local runtime adapter", () => {
289
278
  });
290
279
  });
291
280
 
292
- test("runs apiSource=cli agents through the local CLI executor even when direct provider env is present", async () => {
293
- const executions: Array<{ provider: string; prompt: string; options: any }> = [];
294
- const adapter = createCliLocalRuntimeAdapter({
295
- env: {
296
- OPENAI_API_KEY: "sk-local",
297
- NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
298
- },
299
- cwd: import.meta.dir,
300
- db: {
301
- get: async () => ({
302
- dbKey: "agent-local-frontend",
303
- id: "frontend",
304
- apiSource: "cli",
305
- cliProvider: "agy",
306
- prompt: "Fix UI",
307
- model: "",
308
- }),
309
- put: async () => {},
310
- batch: async () => {},
311
- iterator: () => (async function* () {})(),
312
- },
313
- fetchImpl: async () => {
314
- throw new Error("OpenAI-compatible fetch should not run for local CLI agents");
315
- },
316
- executeCli: async (provider, prompt, options) => {
317
- executions.push({ provider, prompt, options });
318
- return { text: "cli local ok", raw: "cli local ok", elapsed: 10 };
319
- },
320
- });
321
-
322
- const result = await runLocalAgentTurn({
323
- adapter,
324
- agentRef: "frontend",
325
- input: "make it cleaner",
326
- });
327
-
328
- expect(result).toMatchObject({
329
- content: "cli local ok",
330
- model: "agy",
331
- });
332
- expect(executions).toEqual([{
333
- provider: "agy",
334
- prompt: "make it cleaner",
335
- options: expect.objectContaining({
336
- cwd: import.meta.dir,
337
- yolo: true,
338
- systemPrompt: "Fix UI",
339
- }),
340
- }]);
341
- });
342
-
343
- test("uses agent customProviderUrl for local OpenAI-compatible requests", async () => {
344
- const requests: Array<{ url: string; auth: string | null; body: any }> = [];
281
+ test("uses agent-owned custom credentials for local OpenAI-compatible requests", async () => {
282
+ const requests: Array<{ url: string; auth: string | null; apiKeyHeader: string | null; body: any }> = [];
345
283
  const store = new Map<string, any>([
346
284
  ["agent-user-1-custom", {
347
285
  dbKey: "agent-user-1-custom",
@@ -351,6 +289,8 @@ describe("CLI local runtime adapter", () => {
351
289
  provider: "custom-openai-compatible",
352
290
  apiSource: "custom",
353
291
  customProviderUrl: "https://provider.example/v1/chat/completions",
292
+ apiKey: "sk-agent-custom",
293
+ apiKeyHeader: "api-key",
354
294
  temperature: 0.2,
355
295
  max_tokens: 4096,
356
296
  reasoning_effort: "medium",
@@ -381,6 +321,7 @@ describe("CLI local runtime adapter", () => {
381
321
  requests.push({
382
322
  url: String(url),
383
323
  auth: new Headers(init?.headers).get("Authorization"),
324
+ apiKeyHeader: new Headers(init?.headers).get("api-key"),
384
325
  body: JSON.parse(String(init?.body)),
385
326
  });
386
327
  return Response.json({
@@ -399,7 +340,8 @@ describe("CLI local runtime adapter", () => {
399
340
  expect(result.provider).toBe("custom-openai-compatible");
400
341
  expect(requests[0]).toMatchObject({
401
342
  url: "https://provider.example/v1/chat/completions",
402
- auth: "Bearer sk-custom",
343
+ auth: null,
344
+ apiKeyHeader: "sk-agent-custom",
403
345
  body: {
404
346
  model: "custom-coder",
405
347
  messages: [
@@ -415,6 +357,57 @@ describe("CLI local runtime adapter", () => {
415
357
  expect(toolNamesFromRequest(requests[0])).toEqual(DEFAULT_LOCAL_CODING_TOOL_NAMES);
416
358
  });
417
359
 
360
+ test("aborts stalled custom provider requests when timeoutMs is provided", async () => {
361
+ const store = new Map<string, any>([
362
+ ["agent-user-1-mimo", {
363
+ dbKey: "agent-user-1-mimo",
364
+ id: "mimo",
365
+ prompt: "Use monthly mimo.",
366
+ model: "mimo-v2.5-pro",
367
+ provider: "custom",
368
+ apiSource: "custom",
369
+ customProviderUrl: "https://token-plan-cn.xiaomimimo.com/v1",
370
+ apiKey: "mimo-monthly-key",
371
+ }],
372
+ ]);
373
+ const adapter = createCliLocalRuntimeAdapter({
374
+ env: {
375
+ NOLO_LOCAL_USER_ID: "user-1",
376
+ },
377
+ db: {
378
+ get: async (key) => {
379
+ if (!store.has(key)) throw new Error(`not found: ${key}`);
380
+ return store.get(key);
381
+ },
382
+ put: async (key, value) => {
383
+ store.set(key, value);
384
+ },
385
+ batch: async (ops) => {
386
+ for (const op of ops) {
387
+ if (op.type === "put") store.set(op.key, op.value);
388
+ }
389
+ },
390
+ iterator: () => (async function* () {})(),
391
+ },
392
+ fetchImpl: async (_url, init) => {
393
+ if (init?.signal?.aborted) {
394
+ throw init.signal.reason ?? new Error("aborted");
395
+ }
396
+ await new Promise((_, reject) => {
397
+ init?.signal?.addEventListener("abort", () => reject(init.signal?.reason ?? new Error("aborted")));
398
+ });
399
+ throw new Error("unreachable");
400
+ },
401
+ });
402
+
403
+ await expect(runLocalAgentTurn({
404
+ adapter,
405
+ agentRef: "mimo",
406
+ input: "hello",
407
+ timeoutMs: 10,
408
+ })).rejects.toThrow(/timed out|aborted/i);
409
+ });
410
+
418
411
  test("uses loopback transport for localhost custom providers when the default fetch path cannot connect", async () => {
419
412
  const store = new Map<string, any>([
420
413
  ["agent-user-1-localhost", {
@@ -746,10 +739,6 @@ describe("CLI local runtime adapter", () => {
746
739
  id: "shell",
747
740
  prompt: "Use shell.",
748
741
  model: "gpt-4.1-mini",
749
- runtimeToolPolicy: {
750
- version: 1,
751
- runtimeTools: ["execShell"],
752
- },
753
742
  }],
754
743
  ]);
755
744
  const adapter = createCliLocalRuntimeAdapter({
@@ -1054,28 +1043,17 @@ describe("CLI local runtime adapter", () => {
1054
1043
  });
1055
1044
  });
1056
1045
 
1057
- test("rejects execShell when the host disables shell even if the agent requested it", async () => {
1046
+ test("rejects tools unless the local tool policy allows and registers them", async () => {
1058
1047
  const adapter = createCliLocalRuntimeAdapter({
1059
- env: { NOLO_LOCAL_USER_ID: "user-1" },
1060
- hostRuntimeConfig: createCliHostRuntimeConfig(),
1061
- db: {
1062
- get: async () => ({
1063
- dbKey: "agent-user-1-shell",
1064
- toolNames: ["execShell"],
1065
- }),
1066
- put: async () => {},
1067
- batch: async () => {},
1068
- iterator: () => (async function* () {})(),
1069
- },
1048
+ env: {},
1070
1049
  fetchImpl: async () => Response.json({}),
1071
1050
  });
1072
1051
 
1073
- await adapter.loadAgentConfig("shell");
1074
1052
  await expect(adapter.executeTool({
1075
1053
  id: "call-1",
1076
1054
  name: "execShell",
1077
1055
  arguments: "{}",
1078
- })).rejects.toThrow("execShell is not requested by this agent runtime.");
1056
+ })).rejects.toThrow("execShell requires NOLO_LOCAL_SHELL_MODE");
1079
1057
  });
1080
1058
 
1081
1059
  test("executes explicitly allowed registered local tools declared by the agent", async () => {
@@ -1117,22 +1095,19 @@ describe("CLI local runtime adapter", () => {
1117
1095
  expect(result.content).toContain("README.md");
1118
1096
  });
1119
1097
 
1120
- test("defaults the CLI local host runtime to shell-on when runtimeToolPolicy requests execShell", async () => {
1098
+ test("advertises execShell to OpenAI-compatible providers when shell mode is enabled", async () => {
1121
1099
  const requests: Array<{ body: any }> = [];
1122
1100
  const adapter = createCliLocalRuntimeAdapter({
1123
1101
  env: {
1124
1102
  OPENAI_API_KEY: "sk-local",
1125
1103
  NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1104
+ NOLO_LOCAL_SHELL_MODE: "worktree",
1126
1105
  },
1127
1106
  db: {
1128
1107
  get: async () => ({
1129
1108
  dbKey: "agent-local-shell",
1130
1109
  prompt: "Use shell.",
1131
1110
  model: "gpt-4.1-mini",
1132
- runtimeToolPolicy: {
1133
- version: 1,
1134
- runtimeTools: ["execShell"],
1135
- },
1136
1111
  }),
1137
1112
  put: async () => {},
1138
1113
  batch: async () => {},
@@ -1155,81 +1130,6 @@ describe("CLI local runtime adapter", () => {
1155
1130
  expect(toolNamesFromRequest(requests[0])).toEqual(SHELL_LOCAL_CODING_TOOL_NAMES);
1156
1131
  });
1157
1132
 
1158
- test("keeps advertised fallback workspace tools executable when shell is explicitly disabled", async () => {
1159
- const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-cli-runtime-shell-off-"));
1160
- try {
1161
- const adapter = createCliLocalRuntimeAdapter({
1162
- env: {},
1163
- hostRuntimeConfig: createCliHostRuntimeConfig(false),
1164
- db: {
1165
- get: async () => ({
1166
- dbKey: "agent-local-shell-off",
1167
- runtimeToolPolicy: {
1168
- version: 1,
1169
- runtimeTools: ["execShell"],
1170
- },
1171
- }),
1172
- put: async () => {},
1173
- batch: async () => {},
1174
- iterator: () => (async function* () {})(),
1175
- },
1176
- cwd: workspaceRoot,
1177
- fetchImpl: async () => Response.json({}),
1178
- });
1179
-
1180
- await adapter.loadAgentConfig("shell-off");
1181
- const result = await adapter.executeTool({
1182
- id: "call-list-files",
1183
- name: "listFiles",
1184
- arguments: JSON.stringify({ path: "." }),
1185
- });
1186
-
1187
- expect(result.metadata).toMatchObject({ path: ".", count: 0 });
1188
- expect(result.content).toBe("");
1189
- } finally {
1190
- rmSync(workspaceRoot, { recursive: true, force: true });
1191
- }
1192
- });
1193
-
1194
- test("does not advertise execShell when only a legacy runtimeToolPolicy.shell block is present", async () => {
1195
- const requests: Array<{ body: any }> = [];
1196
- const adapter = createCliLocalRuntimeAdapter({
1197
- env: {
1198
- OPENAI_API_KEY: "sk-local",
1199
- NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1200
- },
1201
- hostRuntimeConfig: createCliHostRuntimeConfig(true),
1202
- db: {
1203
- get: async () => ({
1204
- dbKey: "agent-local-legacy-shell",
1205
- prompt: "Do not get shell from legacy policy.",
1206
- model: "gpt-4.1-mini",
1207
- runtimeToolPolicy: {
1208
- version: 1,
1209
- shell: { enabled: true, mode: "worktree" },
1210
- },
1211
- }),
1212
- put: async () => {},
1213
- batch: async () => {},
1214
- iterator: () => (async function* () {})(),
1215
- },
1216
- fetchImpl: async (_url, init) => {
1217
- requests.push({ body: JSON.parse(String(init?.body)) });
1218
- return Response.json({
1219
- choices: [{ message: { content: "done" } }],
1220
- });
1221
- },
1222
- });
1223
-
1224
- await runLocalAgentTurn({
1225
- adapter,
1226
- agentRef: "legacy-shell",
1227
- input: "pwd",
1228
- });
1229
-
1230
- expect(toolNamesFromRequest(requests[0])).not.toContain("execShell");
1231
- });
1232
-
1233
1133
  test("allows default semantic workspace tools without legacy agent tool declarations", async () => {
1234
1134
  const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-cli-runtime-"));
1235
1135
  try {
@@ -1391,17 +1291,13 @@ describe("CLI local runtime adapter", () => {
1391
1291
  }
1392
1292
  });
1393
1293
 
1394
- test("runs execShell locally when the host grants shell capability", async () => {
1294
+ test("runs execShell locally in explicit worktree shell mode", async () => {
1395
1295
  const adapter = createCliLocalRuntimeAdapter({
1396
- env: {},
1397
- hostRuntimeConfig: createCliHostRuntimeConfig(true),
1296
+ env: { NOLO_LOCAL_SHELL_MODE: "worktree" },
1398
1297
  db: {
1399
1298
  get: async () => ({
1400
1299
  dbKey: "agent-local-shell",
1401
- runtimeToolPolicy: {
1402
- version: 1,
1403
- runtimeTools: ["execShell"],
1404
- },
1300
+ toolNames: ["execShell"],
1405
1301
  }),
1406
1302
  put: async () => {},
1407
1303
  batch: async () => {},
@@ -1428,6 +1324,235 @@ describe("CLI local runtime adapter", () => {
1428
1324
  expect(result.metadata).toMatchObject({ exitCode: 0 });
1429
1325
  });
1430
1326
 
1327
+ test("accepts execShell aliases from the local model", async () => {
1328
+ const requests: Array<{ body: any }> = [];
1329
+ const adapter = createCliLocalRuntimeAdapter({
1330
+ env: {
1331
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1332
+ NOLO_LOCAL_SHELL_MODE: "worktree",
1333
+ },
1334
+ db: {
1335
+ get: async () => ({
1336
+ dbKey: "agent-local-shell-alias",
1337
+ prompt: "Use shell when needed.",
1338
+ model: "qwen-coder",
1339
+ toolNames: ["execShell"],
1340
+ }),
1341
+ put: async () => {},
1342
+ batch: async () => {},
1343
+ iterator: () => (async function* () {})(),
1344
+ },
1345
+ cwd: import.meta.dir,
1346
+ localToolExecutors: {
1347
+ execShell: async () => ({
1348
+ content: import.meta.dir,
1349
+ metadata: { exitCode: 0, timedOut: false },
1350
+ }),
1351
+ },
1352
+ fetchImpl: async (_url, init) => {
1353
+ requests.push({ body: JSON.parse(String(init?.body)) });
1354
+ if (requests.length === 1) {
1355
+ return Response.json({
1356
+ choices: [{
1357
+ message: {
1358
+ content: "",
1359
+ tool_calls: [{
1360
+ id: "call-1",
1361
+ type: "function",
1362
+ function: { name: "runCommand", arguments: "{\"cmd\":\"pwd\"}" },
1363
+ }],
1364
+ },
1365
+ }],
1366
+ });
1367
+ }
1368
+ return Response.json({
1369
+ choices: [{ message: { content: "alias ok" } }],
1370
+ });
1371
+ },
1372
+ });
1373
+
1374
+ const result = await runLocalAgentTurn({
1375
+ adapter,
1376
+ agentRef: "shell",
1377
+ input: "print cwd",
1378
+ });
1379
+
1380
+ expect(result.content).toBe("alias ok");
1381
+ expect(requests[1]?.body.messages.at(-1)?.content).toContain(import.meta.dir);
1382
+ });
1383
+
1384
+ test("blocks destructive local execShell calls unless the user explicitly asked to delete", async () => {
1385
+ let executorCalls = 0;
1386
+ const adapter = createCliLocalRuntimeAdapter({
1387
+ env: {
1388
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1389
+ NOLO_LOCAL_SHELL_MODE: "worktree",
1390
+ },
1391
+ db: {
1392
+ get: async () => ({
1393
+ dbKey: "agent-local-shell-guard",
1394
+ prompt: "Use shell when useful.",
1395
+ model: "qwen-coder",
1396
+ toolNames: ["execShell"],
1397
+ }),
1398
+ put: async () => {},
1399
+ batch: async () => {},
1400
+ iterator: () => (async function* () {})(),
1401
+ },
1402
+ cwd: import.meta.dir,
1403
+ localToolExecutors: {
1404
+ execShell: async () => {
1405
+ executorCalls += 1;
1406
+ return {
1407
+ content: "should not run",
1408
+ };
1409
+ },
1410
+ },
1411
+ fetchImpl: async (_url, init) => {
1412
+ const body = JSON.parse(String(init?.body));
1413
+ const lastToolMessage = [...body.messages]
1414
+ .reverse()
1415
+ .find((message: any) => message.role === "tool");
1416
+ if (lastToolMessage) {
1417
+ expect(String(lastToolMessage.content)).toContain(
1418
+ "destructive_action_requires_confirmation",
1419
+ );
1420
+ return Response.json({
1421
+ choices: [{ message: { content: "guard ok" } }],
1422
+ });
1423
+ }
1424
+ return Response.json({
1425
+ choices: [{
1426
+ message: {
1427
+ content: "",
1428
+ tool_calls: [{
1429
+ id: "call-1",
1430
+ type: "function",
1431
+ function: {
1432
+ name: "execShell",
1433
+ arguments: "{\"cmd\":\"rm -rf ./tmp\"}",
1434
+ },
1435
+ }],
1436
+ },
1437
+ }],
1438
+ });
1439
+ },
1440
+ });
1441
+
1442
+ const result = await runLocalAgentTurn({
1443
+ adapter,
1444
+ agentRef: "shell",
1445
+ input: "inspect cwd but don't delete files",
1446
+ });
1447
+
1448
+ expect(result.content).toBe("guard ok");
1449
+ expect(executorCalls).toBe(0);
1450
+ });
1451
+
1452
+ test("exposes runtime policy tools to the local model even when toolNames omits them", async () => {
1453
+ const requests: Array<{ body: any }> = [];
1454
+ const adapter = createCliLocalRuntimeAdapter({
1455
+ env: {
1456
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1457
+ NOLO_LOCAL_SHELL_MODE: "worktree",
1458
+ },
1459
+ db: {
1460
+ get: async () => ({
1461
+ dbKey: "agent-local-policy-shell",
1462
+ prompt: "Use shell when needed.",
1463
+ model: "qwen-coder",
1464
+ runtimeToolPolicy: {
1465
+ version: 1,
1466
+ runtimeTools: ["execShell"],
1467
+ },
1468
+ }),
1469
+ put: async () => {},
1470
+ batch: async () => {},
1471
+ iterator: () => (async function* () {})(),
1472
+ },
1473
+ cwd: import.meta.dir,
1474
+ localToolExecutors: {
1475
+ execShell: async () => ({
1476
+ content: import.meta.dir,
1477
+ metadata: { exitCode: 0, timedOut: false },
1478
+ }),
1479
+ },
1480
+ fetchImpl: async (_url, init) => {
1481
+ requests.push({ body: JSON.parse(String(init?.body)) });
1482
+ if (requests.length === 1) {
1483
+ return Response.json({
1484
+ choices: [{
1485
+ message: {
1486
+ content: "",
1487
+ tool_calls: [{
1488
+ id: "call-1",
1489
+ type: "function",
1490
+ function: { name: "execShell", arguments: "{\"cmd\":\"pwd\"}" },
1491
+ }],
1492
+ },
1493
+ }],
1494
+ });
1495
+ }
1496
+ return Response.json({
1497
+ choices: [{ message: { content: "policy shell ok" } }],
1498
+ });
1499
+ },
1500
+ });
1501
+
1502
+ const result = await runLocalAgentTurn({
1503
+ adapter,
1504
+ agentRef: "shell",
1505
+ input: "print cwd",
1506
+ });
1507
+
1508
+ expect(result.content).toBe("policy shell ok");
1509
+ expect(toolNamesFromRequest(requests[0])).toContain("execShell");
1510
+ expect(requests[1]?.body.messages.at(-1)?.content).toContain(import.meta.dir);
1511
+ });
1512
+
1513
+ test("treats runtime policy visual tools as declared local tools", async () => {
1514
+ const adapter = createCliLocalRuntimeAdapter({
1515
+ env: {
1516
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1517
+ },
1518
+ db: {
1519
+ get: async () => ({
1520
+ dbKey: "agent-local-visual",
1521
+ prompt: "Inspect the page.",
1522
+ model: "qwen-coder",
1523
+ runtimeToolPolicy: {
1524
+ version: 1,
1525
+ runtimeTools: ["captureVisualState"],
1526
+ },
1527
+ }),
1528
+ put: async () => {},
1529
+ batch: async () => {},
1530
+ iterator: () => (async function* () {})(),
1531
+ },
1532
+ cwd: import.meta.dir,
1533
+ localToolExecutors: {
1534
+ captureVisualState: async () => ({
1535
+ content: "{\"status\":\"ok\"}",
1536
+ }),
1537
+ },
1538
+ fetchImpl: async (_url, init) => {
1539
+ expect(toolNamesFromRequest({ body: JSON.parse(String(init?.body)) })).toContain("captureVisualState");
1540
+ return Response.json({
1541
+ choices: [{ message: { content: "visual ok" } }],
1542
+ });
1543
+ },
1544
+ });
1545
+
1546
+ await adapter.loadAgentConfig("visual");
1547
+ await expect(adapter.executeTool({
1548
+ id: "call-visual",
1549
+ name: "captureVisualState",
1550
+ arguments: JSON.stringify({ waitSelector: "body" }),
1551
+ })).resolves.toMatchObject({
1552
+ content: "{\"status\":\"ok\"}",
1553
+ });
1554
+ });
1555
+
1431
1556
  test("runs allowed workspace file tools through default local executors", async () => {
1432
1557
  const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-cli-runtime-"));
1433
1558
  try {