nolo-cli 0.1.36 → 0.1.38

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.
@@ -5,7 +5,6 @@ import {
5
5
  classifyTaskRunReviewStatus,
6
6
  findServerPlatformTools,
7
7
  runAgentTurn,
8
- shouldUseScriptBridge,
9
8
  } from "./agentRun";
10
9
  import { NOLO_PROJECT_MANAGER_AGENT_KEY } from "../agentAliases";
11
10
 
@@ -34,12 +33,6 @@ describe("cli agent run client", () => {
34
33
  expect(classifyTaskRunReviewStatus("I looked at the diff and have notes")).toBeUndefined();
35
34
  });
36
35
 
37
- test("uses the repo script bridge only when no auth token is available", () => {
38
- expect(shouldUseScriptBridge({ hasAuthToken: false, scriptPathExists: true })).toBe(true);
39
- expect(shouldUseScriptBridge({ hasAuthToken: true, scriptPathExists: true })).toBe(false);
40
- expect(shouldUseScriptBridge({ hasAuthToken: false, scriptPathExists: false })).toBe(false);
41
- });
42
-
43
36
  test("identifies server platform tools that local runtime cannot provide", () => {
44
37
  expect(findServerPlatformTools(["readWorkspaceFile", "queryTableRows", "updateTableRow"])).toEqual([
45
38
  "queryTableRows",
@@ -61,7 +54,6 @@ describe("cli agent run client", () => {
61
54
  scriptDir: "C:/missing/scripts",
62
55
  env: { AUTH_TOKEN: "token-123" },
63
56
  output,
64
- scriptPathExists: () => false,
65
57
  fetchImpl: async (url, init) => {
66
58
  requests.push({
67
59
  url: String(url),
@@ -120,7 +112,6 @@ describe("cli agent run client", () => {
120
112
  workItemId: "frontend-filter",
121
113
  artifactIds: ["artifact-1"],
122
114
  },
123
- scriptPathExists: () => false,
124
115
  fetchImpl: async (_url, init) => {
125
116
  requests.push({ body: JSON.parse(String(init?.body)) });
126
117
  return Response.json({ content: "ok", dialogId: "dialog-1" });
@@ -160,7 +151,6 @@ describe("cli agent run client", () => {
160
151
  taskRunRecorder: async (args) => {
161
152
  writebacks.push(args);
162
153
  },
163
- scriptPathExists: () => false,
164
154
  fetchImpl: async () => {
165
155
  return Response.json({ content: "patched", dialogId: "dialog-1" });
166
156
  },
@@ -193,7 +183,6 @@ describe("cli agent run client", () => {
193
183
  env: { AUTH_TOKEN: "token-123" },
194
184
  output,
195
185
  runtimeMode: "server",
196
- scriptPathExists: () => false,
197
186
  fetchImpl: async (_url, init) => {
198
187
  requests.push({ body: JSON.parse(String(init?.body)) });
199
188
  return Response.json({ content: "ok" });
@@ -206,7 +195,7 @@ describe("cli agent run client", () => {
206
195
  ]);
207
196
  });
208
197
 
209
- test("sends script-compatible dialog metadata over HTTP", async () => {
198
+ test("sends CLI dialog metadata over HTTP", async () => {
210
199
  const output = new CaptureOutput();
211
200
  const requests: Array<{ body: any }> = [];
212
201
 
@@ -227,7 +216,6 @@ describe("cli agent run client", () => {
227
216
  env: { AUTH_TOKEN: "token-123" },
228
217
  output,
229
218
  runtimeMode: "server",
230
- scriptPathExists: () => false,
231
219
  fetchImpl: async (_url, init) => {
232
220
  requests.push({ body: JSON.parse(String(init?.body)) });
233
221
  return Response.json({ dialogId: "dialog-1", status: "queued" });
@@ -271,7 +259,6 @@ describe("cli agent run client", () => {
271
259
  writebacks.push(args);
272
260
  },
273
261
  output,
274
- scriptPathExists: () => false,
275
262
  fetchImpl: async () => {
276
263
  return Response.json({ dialogId: "dialog-bg", status: "pending" }, { status: 202 });
277
264
  },
@@ -319,7 +306,6 @@ describe("cli agent run client", () => {
319
306
  throw new Error("no tools expected");
320
307
  },
321
308
  },
322
- scriptPathExists: () => false,
323
309
  fetchImpl: async () => {
324
310
  throw new Error("HTTP should not be called for forced local runs");
325
311
  },
@@ -371,7 +357,6 @@ describe("cli agent run client", () => {
371
357
  }),
372
358
  executeTool: async () => ({ content: "ok" }),
373
359
  },
374
- scriptPathExists: () => false,
375
360
  fetchImpl: async () => {
376
361
  throw new Error("HTTP should not be called for forced local runs");
377
362
  },
@@ -432,7 +417,6 @@ describe("cli agent run client", () => {
432
417
  return { content: "ok" };
433
418
  },
434
419
  },
435
- scriptPathExists: () => false,
436
420
  });
437
421
 
438
422
  expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-retry" });
@@ -487,7 +471,6 @@ describe("cli agent run client", () => {
487
471
  }),
488
472
  executeTool: async () => ({ content: "clean" }),
489
473
  },
490
- scriptPathExists: () => false,
491
474
  });
492
475
 
493
476
  expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
@@ -548,7 +531,6 @@ describe("cli agent run client", () => {
548
531
  }),
549
532
  executeTool: async () => ({ content: "clean" }),
550
533
  },
551
- scriptPathExists: () => false,
552
534
  });
553
535
 
554
536
  expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
@@ -604,7 +586,6 @@ describe("cli agent run client", () => {
604
586
  throw new Error("no tools expected");
605
587
  },
606
588
  },
607
- scriptPathExists: () => false,
608
589
  });
609
590
 
610
591
  expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-bg" });
@@ -648,7 +629,6 @@ describe("cli agent run client", () => {
648
629
  throw new Error("no tools expected");
649
630
  },
650
631
  },
651
- scriptPathExists: () => false,
652
632
  fetchImpl: async (url) => {
653
633
  httpCalls.push(String(url));
654
634
  return Response.json({ content: "server" });
@@ -701,7 +681,6 @@ describe("cli agent run client", () => {
701
681
  throw new Error("no tools expected");
702
682
  },
703
683
  },
704
- scriptPathExists: () => false,
705
684
  fetchImpl: async (url) => {
706
685
  httpCalls.push(String(url));
707
686
  return Response.json({ content: "server" });
@@ -748,7 +727,6 @@ describe("cli agent run client", () => {
748
727
  throw new Error("no tools expected");
749
728
  },
750
729
  },
751
- scriptPathExists: () => false,
752
730
  fetchImpl: async (url, init) => {
753
731
  httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
754
732
  return Response.json({ content: "server fallback", dialogId: "dialog-server-fallback" });
@@ -798,7 +776,6 @@ describe("cli agent run client", () => {
798
776
  throw new Error("local tools should be skipped");
799
777
  },
800
778
  },
801
- scriptPathExists: () => false,
802
779
  fetchImpl: async (url, init) => {
803
780
  httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
804
781
  return Response.json({ content: "server ok", dialogId: "dialog-server" });
@@ -852,7 +829,6 @@ describe("cli agent run client", () => {
852
829
  throw new Error("local tools should be skipped");
853
830
  },
854
831
  },
855
- scriptPathExists: () => false,
856
832
  fetchImpl: async (url, init) => {
857
833
  httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
858
834
  return Response.json({ content: "server ok", dialogId: "dialog-server-policy" });
@@ -898,7 +874,6 @@ describe("cli agent run client", () => {
898
874
  throw new Error("local tools should be skipped");
899
875
  },
900
876
  },
901
- scriptPathExists: () => false,
902
877
  fetchImpl: async (url, init) => {
903
878
  httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
904
879
  return Response.json({ content: "server ok", dialogId: "dialog-server" });
@@ -942,7 +917,6 @@ describe("cli agent run client", () => {
942
917
  },
943
918
  };
944
919
  },
945
- scriptPathExists: () => false,
946
920
  fetchImpl: async () => {
947
921
  throw new Error("HTTP should not be called for env local runs");
948
922
  },
@@ -964,7 +938,6 @@ describe("cli agent run client", () => {
964
938
  scriptDir: "C:/missing/scripts",
965
939
  env: { AUTH_TOKEN: "token-123" },
966
940
  output,
967
- scriptPathExists: () => false,
968
941
  fetchImpl: async () =>
969
942
  new Response(
970
943
  [
@@ -998,7 +971,6 @@ describe("cli agent run client", () => {
998
971
  scriptDir: "C:/missing/scripts",
999
972
  env: { AUTH_TOKEN: "token-123" },
1000
973
  output,
1001
- scriptPathExists: () => false,
1002
974
  fetchImpl: async () => {
1003
975
  let sent = false;
1004
976
  return new Response(
@@ -1054,14 +1026,13 @@ describe("cli agent run client", () => {
1054
1026
  scriptDir: "C:/missing/scripts",
1055
1027
  env: {},
1056
1028
  output,
1057
- scriptPathExists: () => false,
1058
1029
  fetchImpl: async () => {
1059
1030
  throw new Error("fetch should not be called");
1060
1031
  },
1061
1032
  });
1062
1033
 
1063
1034
  expect(result.exitCode).toBe(1);
1064
- expect(output.text()).toContain("Set AUTH_TOKEN");
1035
+ expect(output.text()).toContain("Run `nolo login`");
1065
1036
  });
1066
1037
 
1067
1038
  test("prints a friendly connection hint instead of crashing on transport errors", async () => {
@@ -1075,7 +1046,6 @@ describe("cli agent run client", () => {
1075
1046
  scriptDir: "C:/missing/scripts",
1076
1047
  env: { AUTH_TOKEN: "token-123" },
1077
1048
  output,
1078
- scriptPathExists: () => false,
1079
1049
  fetchImpl: async () => {
1080
1050
  throw new Error("ConnectionRefused");
1081
1051
  },
@@ -1,22 +1,20 @@
1
- import { existsSync } from "node:fs";
2
1
  import { join } from "node:path";
3
2
  import { runLocalAgentTurn } from "../agentRuntimeLocal";
4
3
  import { LOCAL_AGENT_CONFIG_MISSING_CODE } from "../agent-runtime/localLoop";
5
4
  import {
6
- FRONTEND_IMPLEMENTER_AGENT_KEY,
7
- NOLO_FULLSTACK_AGENT_KEY,
5
+ MIMO_MONTH_AGENT_KEY,
8
6
  NOLO_PROJECT_MANAGER_AGENT_KEY,
9
- NOLO_REVIEW_AGENT_KEY,
10
7
  NOLO_SENIOR_FULLSTACK_AGENT_KEY,
8
+ WIN_CODEX_AGENT_KEY,
11
9
  } from "../agentAliases";
12
10
  import type { LocalAgentToolEvent } from "../agent-runtime/localLoop";
13
11
  import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
14
12
  import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
15
13
  import { createStreamingTextWriter } from "./streamingOutput";
16
14
  import { prependTaskRunPrompt, type TaskRunPromptContext } from "./taskRunPrompt";
17
-
18
- type EnvLike = Record<string, string | undefined>;
19
-
15
+
16
+ type EnvLike = Record<string, string | undefined>;
17
+
20
18
  type OutputLike = {
21
19
  write(chunk: string): unknown;
22
20
  };
@@ -48,7 +46,7 @@ type RunAgentTurnOptions = {
48
46
  noStream?: boolean;
49
47
  noDefaultTestRoot?: boolean;
50
48
  scriptDir: string;
51
- env: EnvLike;
49
+ env: EnvLike;
52
50
  output: OutputLike;
53
51
  runtimeMode?: AgentRuntimeRequestedMode;
54
52
  localRuntimeAdapter?: AgentRuntimeHostAdapter;
@@ -66,20 +64,55 @@ type RunAgentTurnOptions = {
66
64
  errorCode?: string;
67
65
  toolEvidence?: TaskRunToolEvidence;
68
66
  }) => Promise<void>;
69
- scriptPathExists?: (path: string) => boolean;
70
67
  fetchImpl?: typeof fetch;
71
68
  };
72
-
69
+
73
70
  export type RunAgentTurnResult = {
74
71
  exitCode: number;
75
72
  dialogId?: string;
76
73
  streamInterrupted?: boolean;
77
74
  };
78
-
79
- type ScriptBridgeDecision = {
80
- hasAuthToken: boolean;
81
- scriptPathExists: boolean;
82
- };
75
+
76
+ class Spinner {
77
+ private timer: any = null;
78
+ private frameIndex = 0;
79
+ private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
80
+ private isTTY: boolean;
81
+
82
+ constructor(
83
+ private output: OutputLike,
84
+ private text: string
85
+ ) {
86
+ this.isTTY = Boolean((output as any).isTTY ?? process.stdout.isTTY);
87
+ }
88
+
89
+ start() {
90
+ if (!this.isTTY) {
91
+ this.output.write(`\n${this.text}...\n`);
92
+ return;
93
+ }
94
+
95
+ this.frameIndex = 0;
96
+ this.output.write(`\x1b[36m${this.frames[0]}\x1b[39m ${this.text}...`);
97
+
98
+ this.timer = setInterval(() => {
99
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
100
+ this.output.write(`\r\x1b[36m${this.frames[this.frameIndex]}\x1b[39m ${this.text}...`);
101
+ }, 80);
102
+ }
103
+
104
+ stop() {
105
+ if (this.timer) {
106
+ clearInterval(this.timer);
107
+ this.timer = null;
108
+ }
109
+
110
+ if (this.isTTY) {
111
+ this.output.write("\r\x1b[K");
112
+ }
113
+ }
114
+ }
115
+
83
116
 
84
117
  const SERVER_PLATFORM_TOOL_NAMES = new Set([
85
118
  "addTableRow",
@@ -94,10 +127,9 @@ const SERVER_PLATFORM_TOOL_NAMES = new Set([
94
127
  ]);
95
128
 
96
129
  const KNOWN_SERVER_PLATFORM_AGENT_KEYS = new Set([
97
- FRONTEND_IMPLEMENTER_AGENT_KEY,
98
- NOLO_FULLSTACK_AGENT_KEY,
130
+ MIMO_MONTH_AGENT_KEY,
99
131
  NOLO_PROJECT_MANAGER_AGENT_KEY,
100
- NOLO_REVIEW_AGENT_KEY,
132
+ WIN_CODEX_AGENT_KEY,
101
133
  NOLO_SENIOR_FULLSTACK_AGENT_KEY,
102
134
  ]);
103
135
 
@@ -125,10 +157,6 @@ const KNOWN_SERVER_PLATFORM_AGENT_ALIASES = new Set([
125
157
  "reviewer",
126
158
  ]);
127
159
 
128
- export function shouldUseScriptBridge(decision: ScriptBridgeDecision) {
129
- return !decision.hasAuthToken && decision.scriptPathExists;
130
- }
131
-
132
160
  export function findServerPlatformTools(toolNames?: string[]) {
133
161
  if (!Array.isArray(toolNames)) return [];
134
162
  return toolNames.filter((toolName) => SERVER_PLATFORM_TOOL_NAMES.has(toolName));
@@ -514,62 +542,9 @@ function buildTransportErrorHint(serverUrl: string, error: unknown) {
514
542
  return detail;
515
543
  }
516
544
 
517
- async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string) {
518
- options.output.write(`\n${options.agentName} -> working via local script...\n\n`);
519
- const proc = Bun.spawn({
520
- cmd: [
521
- process.execPath,
522
- scriptPath,
523
- "--agent",
524
- options.agentKey,
525
- "--server",
526
- options.serverUrl,
527
- "--msg",
528
- options.message,
529
- ...(options.taskRunContext?.rowDbKey
530
- ? ["--task-row-dbkey", options.taskRunContext.rowDbKey]
531
- : []),
532
- ...(options.taskRunContext?.taskRunId
533
- ? ["--task-run-id", options.taskRunContext.taskRunId]
534
- : []),
535
- ...(options.taskRunContext?.workItemId
536
- ? ["--work-item-id", options.taskRunContext.workItemId]
537
- : []),
538
- ...(options.taskRunContext?.artifactIds?.length
539
- ? ["--artifact-ids", options.taskRunContext.artifactIds.join(",")]
540
- : []),
541
- ...(options.noDefaultTestRoot ? ["--no-default-test-root"] : []),
542
- ...(options.continueDialogId
543
- ? ["--continue", options.continueDialogId]
544
- : []),
545
- ...(options.spaceId ? ["--space", options.spaceId] : []),
546
- ...(options.category ? ["--category", options.category] : []),
547
- ...(options.inheritedFromDialogKey
548
- ? ["--inherit-from-dialog", options.inheritedFromDialogKey]
549
- : []),
550
- ...(options.background ? ["--bg"] : []),
551
- ...(typeof options.timeoutMs === "number" ? ["--timeout-ms", String(options.timeoutMs)] : []),
552
- ...(options.noStream ? ["--no-stream"] : []),
553
- ],
554
- stdin: "inherit",
555
- stdout: "inherit",
556
- stderr: "inherit",
557
- env: {
558
- ...process.env,
559
- ...options.env,
560
- ASSISTANT_LABEL: options.agentName,
561
- },
562
- });
563
-
564
- const exitCode = await proc.exited;
565
- if (exitCode !== 0) {
566
- options.output.write(`\n[nolo] Agent run exited with code ${exitCode}.\n`);
567
- }
568
- return { exitCode };
569
- }
570
-
571
545
  async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
572
- options.output.write(`\n${options.agentName} -> working...\n`);
546
+ const spinner = new Spinner(options.output, `${options.agentName} -> working`);
547
+ spinner.start();
573
548
 
574
549
  const fetchImpl = options.fetchImpl ?? fetch;
575
550
  let res: Response;
@@ -623,14 +598,16 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
623
598
  ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
624
599
  stream: !options.noStream && !options.background,
625
600
  }),
626
- });
627
- } catch (error) {
628
- options.output.write(buildTransportErrorHint(options.serverUrl, error));
629
- return { exitCode: 1 };
630
- }
631
-
601
+ });
602
+ } catch (error) {
603
+ spinner.stop();
604
+ options.output.write(buildTransportErrorHint(options.serverUrl, error));
605
+ return { exitCode: 1 };
606
+ }
607
+
632
608
  const contentType = res.headers.get("content-type") || "";
633
609
  if (contentType.includes("text/event-stream") && res.body) {
610
+ spinner.stop();
634
611
  const result = await readStreamingAgentRun(options, res);
635
612
  await recordTaskRunOutcome({
636
613
  options,
@@ -644,14 +621,15 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
644
621
  });
645
622
  return result;
646
623
  }
647
-
648
- let data: any = {};
649
- try {
650
- data = await res.json();
651
- } catch {
652
- data = {};
653
- }
654
-
624
+
625
+ spinner.stop();
626
+ let data: any = {};
627
+ try {
628
+ data = await res.json();
629
+ } catch {
630
+ data = {};
631
+ }
632
+
655
633
  if (!res.ok) {
656
634
  options.output.write(`[nolo] Agent request failed: HTTP ${res.status}\n`);
657
635
  if (data?.error || data?.message) {
@@ -758,7 +736,8 @@ async function runLocalAgentTurnForCli(
758
736
  return { exitCode: 1 };
759
737
  }
760
738
 
761
- options.output.write(`\n${options.agentName} -> working locally...\n`);
739
+ const spinner = new Spinner(options.output, `${options.agentName} -> working locally`);
740
+ spinner.start();
762
741
  const toolEvents: LocalAgentToolEvent[] = [];
763
742
  try {
764
743
  const traceLocalTools = shouldTraceLocalTools(options);
@@ -783,11 +762,15 @@ async function runLocalAgentTurnForCli(
783
762
  ? {
784
763
  onToolEvent: (event) => {
785
764
  if (collectToolEvidence) toolEvents.push(event);
786
- if (traceLocalTools) options.output.write(formatToolTraceEvent(event));
765
+ if (traceLocalTools) {
766
+ spinner.stop();
767
+ options.output.write(formatToolTraceEvent(event));
768
+ }
787
769
  },
788
770
  }
789
771
  : {}),
790
772
  });
773
+ spinner.stop();
791
774
  const content = result.content.trim();
792
775
  if (content) {
793
776
  options.output.write(`\n${options.agentName} > ${content}\n`);
@@ -803,6 +786,7 @@ async function runLocalAgentTurnForCli(
803
786
  });
804
787
  return { exitCode: 0, dialogId: result.dialogId };
805
788
  } catch (error) {
789
+ spinner.stop();
806
790
  if (settings.reportFailure) {
807
791
  options.output.write(
808
792
  `[nolo] Local agent run failed: ${
@@ -927,8 +911,6 @@ async function readStreamingAgentRun(
927
911
 
928
912
  export async function runAgentTurn(options: RunAgentTurnOptions) {
929
913
  const authToken = resolveAuthToken(options.env);
930
- const scriptPath = join(options.scriptDir, "chatWithAgent.ts");
931
- const scriptPathExists = (options.scriptPathExists ?? existsSync)(scriptPath);
932
914
  const runtimeMode = resolveRequestedRuntimeMode(options);
933
915
 
934
916
  if (runtimeMode === "local") {
@@ -969,17 +951,13 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
969
951
  }
970
952
  }
971
953
 
972
- if (shouldUseScriptBridge({ hasAuthToken: Boolean(authToken), scriptPathExists })) {
973
- return runScriptBridge(options, scriptPath);
954
+ if (!authToken) {
955
+ options.output.write(
956
+ "[nolo] This install needs an auth token before it can talk to agents.\n" +
957
+ "Run `nolo login`, or set AUTH_TOKEN / NOLO_SERVER for non-interactive runs.\n"
958
+ );
959
+ return { exitCode: 1 };
974
960
  }
975
961
 
976
- if (!authToken) {
977
- options.output.write(
978
- "[nolo] This install needs an auth token before it can talk to agents.\n" +
979
- "Set AUTH_TOKEN, or use the repo-local CLI where the dev script bridge is available.\n"
980
- );
981
- return { exitCode: 1 };
982
- }
983
-
984
962
  return runHttpAgentTurn(options, authToken);
985
963
  }
@@ -1324,6 +1324,70 @@ describe("CLI local runtime adapter", () => {
1324
1324
  expect(result.metadata).toMatchObject({ exitCode: 0 });
1325
1325
  });
1326
1326
 
1327
+ test("applies runtime policy shell output limit to local executors without adding a timeout", 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-limits",
1337
+ prompt: "Use shell when needed.",
1338
+ model: "qwen-coder",
1339
+ runtimeToolPolicy: {
1340
+ version: 1,
1341
+ runtimeTools: ["execShell"],
1342
+ shell: { enabled: true, mode: "worktree", maxOutputBytes: 120 },
1343
+ },
1344
+ }),
1345
+ put: async () => {},
1346
+ batch: async () => {},
1347
+ iterator: () => (async function* () {})(),
1348
+ },
1349
+ cwd: import.meta.dir,
1350
+ fetchImpl: async (_url, init) => {
1351
+ requests.push({ body: JSON.parse(String(init?.body)) });
1352
+ if (requests.length === 1) {
1353
+ return Response.json({
1354
+ choices: [{
1355
+ message: {
1356
+ content: "",
1357
+ tool_calls: [{
1358
+ id: "call-1",
1359
+ type: "function",
1360
+ function: {
1361
+ name: "execShell",
1362
+ arguments: JSON.stringify({
1363
+ cmd: process.platform === "win32"
1364
+ ? "'abcdefghijklmnopqrstuvwxyz0123456789'.PadRight(500, 'x')"
1365
+ : "bun -e 'console.log(\"x\".repeat(500))'",
1366
+ }),
1367
+ },
1368
+ }],
1369
+ },
1370
+ }],
1371
+ });
1372
+ }
1373
+ return Response.json({
1374
+ choices: [{ message: { content: "limits applied" } }],
1375
+ });
1376
+ },
1377
+ });
1378
+
1379
+ const result = await runLocalAgentTurn({
1380
+ adapter,
1381
+ agentRef: "shell-limits",
1382
+ input: "run output limit check",
1383
+ });
1384
+
1385
+ expect(result.content).toBe("limits applied");
1386
+ const toolResult = requests[1]?.body.messages.at(-1)?.content ?? "";
1387
+ expect(toolResult).toContain("[truncated ");
1388
+ expect(toolResult).not.toContain("command timed out");
1389
+ });
1390
+
1327
1391
  test("accepts execShell aliases from the local model", async () => {
1328
1392
  const requests: Array<{ body: any }> = [];
1329
1393
  const adapter = createCliLocalRuntimeAdapter({
@@ -16,6 +16,8 @@ import {
16
16
  parsePlatformChatCompletionData,
17
17
  parsePlatformChatCompletionResponse,
18
18
  resolvePlatformChatProviderConfig,
19
+ resolveCurrentRunRuntimeToolPolicy,
20
+ resolveLocalWorkspaceExecutorOptionsFromPolicy,
19
21
  resolveRequestedRuntimeToolNames,
20
22
  shouldUsePlatformChatProvider,
21
23
  } from "../agentRuntimeLocal";
@@ -337,9 +339,15 @@ function buildLocalToolExecutors(args: {
337
339
  env: EnvLike;
338
340
  fetchImpl: typeof fetch;
339
341
  localToolExecutors?: CliLocalRuntimeAdapterDeps["localToolExecutors"];
342
+ commandTimeoutMs?: number;
343
+ commandOutputLimit?: number;
340
344
  }) {
341
345
  return {
342
- ...createLocalWorkspaceToolExecutors({ workspaceRoot: args.workspaceRoot }),
346
+ ...createLocalWorkspaceToolExecutors({
347
+ workspaceRoot: args.workspaceRoot,
348
+ commandTimeoutMs: args.commandTimeoutMs,
349
+ commandOutputLimit: args.commandOutputLimit,
350
+ }),
343
351
  ...buildServerPlatformToolExecutors({
344
352
  env: args.env,
345
353
  fetchImpl: args.fetchImpl,
@@ -434,11 +442,13 @@ export function createCliLocalRuntimeAdapter(
434
442
  ? { defaultCwd: deps.cwd }
435
443
  : { cwd: deps.cwd }
436
444
  );
445
+ let runtimeToolExecutionLimits: ReturnType<typeof resolveLocalWorkspaceExecutorOptionsFromPolicy> = {};
437
446
  let localToolExecutors = buildLocalToolExecutors({
438
447
  workspaceRoot: workspaceSession.workspaceRoot,
439
448
  env: deps.env,
440
449
  fetchImpl,
441
450
  localToolExecutors: deps.localToolExecutors,
451
+ ...runtimeToolExecutionLimits,
442
452
  });
443
453
 
444
454
  async function activateAgentWorkspaceSession(args: {
@@ -458,6 +468,7 @@ export function createCliLocalRuntimeAdapter(
458
468
  env: deps.env,
459
469
  fetchImpl,
460
470
  localToolExecutors: deps.localToolExecutors,
471
+ ...runtimeToolExecutionLimits,
461
472
  });
462
473
  deps.output?.write(formatWorkspaceSessionActivation(workspaceSession));
463
474
  }
@@ -478,7 +489,17 @@ export function createCliLocalRuntimeAdapter(
478
489
  toolNames: requestedToolNames,
479
490
  env: deps.env,
480
491
  });
492
+ runtimeToolExecutionLimits = resolveLocalWorkspaceExecutorOptionsFromPolicy(
493
+ resolveCurrentRunRuntimeToolPolicy(agentConfig),
494
+ );
481
495
  await activateAgentWorkspaceSession({ agentRef, agentConfig });
496
+ localToolExecutors = buildLocalToolExecutors({
497
+ workspaceRoot: workspaceSession.workspaceRoot,
498
+ env: deps.env,
499
+ fetchImpl,
500
+ localToolExecutors: deps.localToolExecutors,
501
+ ...runtimeToolExecutionLimits,
502
+ });
482
503
  return agentConfig;
483
504
  },
484
505
  loadDialogHistory: async (dialogId) => readDialogMessages({
@@ -1,4 +1,5 @@
1
1
  import { runLocalAgentTurn, type LocalAgentToolEvent } from "./agent-runtime/localLoop";
2
+ import { resolveLocalRuntimeEnvFromPolicy } from "./agent-runtime/runtimeToolPolicy";
2
3
  import {
3
4
  assertMachineRunAllowed as defaultAssertMachineRunAllowed,
4
5
  resolveMachineRunPermissionPolicy as defaultResolveMachineRunPermissionPolicy,
@@ -225,11 +226,8 @@ function mergeToolNames(...values: unknown[]) {
225
226
  return names;
226
227
  }
227
228
 
228
- function localRuntimeEnvFromPolicy(runtimeEnv: EnvLike, policy?: ConnectorRuntimePolicy): EnvLike {
229
- return {
230
- ...runtimeEnv,
231
- ...(policy?.workspace?.cwd ? { NOLO_LOCAL_WORKTREE: policy.workspace.cwd } : {}),
232
- };
229
+ export function localRuntimeEnvFromPolicy(runtimeEnv: EnvLike, policy?: ConnectorRuntimePolicy): EnvLike {
230
+ return resolveLocalRuntimeEnvFromPolicy(runtimeEnv, policy);
233
231
  }
234
232
 
235
233
  function forwardedUserAuthToken(parsed: any): string {