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.
package/README.md CHANGED
@@ -76,7 +76,8 @@ token directly:
76
76
  nolo login --server https://nolo.chat --token <token>
77
77
  ```
78
78
 
79
- Local repo development can still use the script bridge without `AUTH_TOKEN`.
79
+ Local repo development should use `nolo login` or an explicit `AUTH_TOKEN` for
80
+ agent runs.
80
81
 
81
82
  Inside the TUI, `/update` is the shortcut for the same global `nolo update`
82
83
  command.
@@ -207,6 +208,15 @@ commands instead of ad hoc scripts:
207
208
  updates such as `--model`, `--cli-provider`, `--api-source`, and repeated
208
209
  `--field key=value`.
209
210
 
211
+ Account ownership matters for private agent maintenance. The legacy
212
+ `b2e06f801f` / platform-demo account still owns older agents, task-board data,
213
+ and workspace records, while newer personal or machine-bound work may belong to
214
+ the current operator account. Use `nolo agent read`, `nolo agent update`, and
215
+ repo-native helpers as the normal entrypoints; only owner-only operations such
216
+ as private record updates/deletes should use the maintained platform-demo token
217
+ from `scripts/testUtils.ts` or a regenerated token from `scripts/generateToken.ts`.
218
+ Do not copy token strings into docs, logs, or prompts.
219
+
210
220
  Inside the current TUI, examples of supported slash commands include:
211
221
  `/agent`, `/agents`, `/switch`, `/context` (alias `/ctx`), `/dialog`, `/doc`,
212
222
  `/help`, `/new`, `/customize`, `/login`, `/profile`, `/version`, `/quit`
@@ -33,6 +33,7 @@ function localWorkspaceModeField(record: AgentRecord): "current" | "task-worktre
33
33
 
34
34
  function appendUniqueStrings(values: string[], next: unknown) {
35
35
  if (!Array.isArray(next)) return values;
36
+ const seen = new Set(values);
36
37
  for (const value of next) {
37
38
  const toolName = typeof value === "string"
38
39
  ? value
@@ -41,7 +42,10 @@ function appendUniqueStrings(values: string[], next: unknown) {
41
42
  : value && typeof value === "object" && typeof (value as any).function?.name === "string"
42
43
  ? (value as any).function.name
43
44
  : "";
44
- if (toolName && !values.includes(toolName)) values.push(toolName);
45
+ if (toolName && !seen.has(toolName)) {
46
+ seen.add(toolName);
47
+ values.push(toolName);
48
+ }
45
49
  }
46
50
  return values;
47
51
  }
@@ -59,8 +59,11 @@ export {
59
59
  export {
60
60
  mergeAgentRuntimeToolPolicies,
61
61
  normalizeAgentRuntimeToolPolicy,
62
+ resolveCurrentRunRuntimeToolPolicy,
62
63
  resolveRequestedRuntimeToolNames,
63
64
  resolveEffectiveRuntimeToolPolicy,
65
+ resolveLocalRuntimeEnvFromPolicy,
66
+ resolveLocalWorkspaceExecutorOptionsFromPolicy,
64
67
  } from "./runtimeToolPolicy";
65
68
  export {
66
69
  buildLocalWorkspaceOpenAiTools,
@@ -94,8 +94,7 @@ function extractUserInputText(content: AgentRuntimeMessageContent): string {
94
94
  if (typeof content === "string") return content;
95
95
  if (!Array.isArray(content)) return "";
96
96
  return content
97
- .map((part) => (part?.type === "text" ? part.text : ""))
98
- .filter(Boolean)
97
+ .flatMap((part) => (part?.type === "text" && part.text ? [part.text] : []))
99
98
  .join("\n")
100
99
  .trim();
101
100
  }
@@ -38,16 +38,16 @@ const DEFAULT_LOCAL_TOOLS = new Set([
38
38
  "editFile",
39
39
  "searchFiles",
40
40
  "searchWorkspace",
41
- "applyPatch",
42
- "runPackageScript",
43
41
  "execShell",
44
42
  ]);
45
43
 
46
44
  function parseToolAllowlist(value: string | undefined) {
47
45
  return (value ?? "")
48
46
  .split(",")
49
- .map((item) => item.trim())
50
- .filter(Boolean);
47
+ .flatMap((item) => {
48
+ const trimmed = item.trim();
49
+ return trimmed ? [trimmed] : [];
50
+ });
51
51
  }
52
52
 
53
53
  function normalizeLocalToolName(toolName: string) {
@@ -129,7 +129,10 @@ export function resolveLocalToolPolicy(args: {
129
129
 
130
130
  const allowedByEnv = new Set(parseToolAllowlist(args.env.NOLO_LOCAL_ALLOWED_TOOLS));
131
131
  const agentTools = new Set(
132
- (args.agentToolNames ?? []).map(normalizeLocalToolName).filter(Boolean),
132
+ (args.agentToolNames ?? []).flatMap((item) => {
133
+ const normalized = normalizeLocalToolName(item);
134
+ return normalized ? [normalized] : [];
135
+ }),
133
136
  );
134
137
  if (!restrictedMode && agentTools.has(toolName)) {
135
138
  return { allowed: true, toolName };
@@ -9,6 +9,7 @@ import type {
9
9
  type LocalWorkspaceToolArgs = {
10
10
  workspaceRoot: string;
11
11
  commandTimeoutMs?: number;
12
+ commandOutputLimit?: number;
12
13
  commandPrefix?: string[];
13
14
  };
14
15
 
@@ -21,9 +22,6 @@ type WorkspaceFileArgs = {
21
22
  query?: unknown;
22
23
  command?: unknown;
23
24
  cmd?: unknown;
24
- patch?: unknown;
25
- script?: unknown;
26
- args?: unknown;
27
25
  branch?: unknown;
28
26
  paths?: unknown;
29
27
  message?: unknown;
@@ -56,8 +54,6 @@ const WORKSPACE_TOOL_NAMES = [
56
54
  "searchWorkspace",
57
55
  "applyEdit",
58
56
  "applyLineEdits",
59
- "applyPatch",
60
- "runPackageScript",
61
57
  "startPreview",
62
58
  "getPreviewStatus",
63
59
  "stopPreview",
@@ -84,7 +80,6 @@ const REMOVED_WORKSPACE_TOOL_NAMES = new Set([
84
80
  "gitCommit",
85
81
  "commitWorkspace",
86
82
  ]);
87
- const DEFAULT_WORKSPACE_COMMAND_TIMEOUT_MS = 120_000;
88
83
 
89
84
  function buildWorkspacePathProperty() {
90
85
  return {
@@ -200,51 +195,6 @@ function buildSearchWorkspaceTool(): OpenAiCompatibleTool {
200
195
  };
201
196
  }
202
197
 
203
- function buildApplyPatchTool(): OpenAiCompatibleTool {
204
- return {
205
- type: "function",
206
- function: {
207
- name: "applyPatch",
208
- description: "Apply a unified diff patch to files inside the workspace.",
209
- parameters: {
210
- type: "object",
211
- properties: {
212
- patch: {
213
- type: "string",
214
- description: "Unified diff text accepted by git apply.",
215
- },
216
- },
217
- required: ["patch"],
218
- },
219
- },
220
- };
221
- }
222
-
223
- function buildRunPackageScriptTool(): OpenAiCompatibleTool {
224
- return {
225
- type: "function",
226
- function: {
227
- name: "runPackageScript",
228
- description: "Run a package script in the workspace, for example a test or lint script.",
229
- parameters: {
230
- type: "object",
231
- properties: {
232
- script: {
233
- type: "string",
234
- description: "Package script name to run.",
235
- },
236
- args: {
237
- type: "array",
238
- items: { type: "string" },
239
- description: "Optional extra arguments passed after --.",
240
- },
241
- },
242
- required: ["script"],
243
- },
244
- },
245
- };
246
- }
247
-
248
198
  function buildPreviewLifecycleTool(toolName: string): OpenAiCompatibleTool {
249
199
  const descriptions: Record<string, string> = {
250
200
  startPreview: "Start the local preview stack for the current workspace.",
@@ -421,8 +371,6 @@ function buildWorkspaceToolDefinition(toolName: string) {
421
371
  if (toolName === "searchWorkspace" || toolName === "searchFiles") {
422
372
  return renameWorkspaceToolDefinition(buildSearchWorkspaceTool(), toolName);
423
373
  }
424
- if (toolName === "applyPatch") return buildApplyPatchTool();
425
- if (toolName === "runPackageScript") return buildRunPackageScriptTool();
426
374
  if (toolName === "startPreview" || toolName === "getPreviewStatus" || toolName === "stopPreview" || toolName === "releasePreview") {
427
375
  return buildPreviewLifecycleTool(toolName);
428
376
  }
@@ -585,12 +533,6 @@ function requireWorkspaceSearchQuery(args: WorkspaceFileArgs) {
585
533
  return query;
586
534
  }
587
535
 
588
- function requireWorkspacePatch(args: WorkspaceFileArgs) {
589
- const patch = typeof args.patch === "string" ? args.patch : "";
590
- if (!patch.trim()) throw new Error("applyPatch requires a non-empty patch.");
591
- return patch;
592
- }
593
-
594
536
  function requireShellCommand(args: WorkspaceFileArgs, toolName: string) {
595
537
  const command = typeof args.cmd === "string"
596
538
  ? args.cmd.trim()
@@ -601,24 +543,12 @@ function requireShellCommand(args: WorkspaceFileArgs, toolName: string) {
601
543
  return command;
602
544
  }
603
545
 
604
- function requirePackageScript(args: WorkspaceFileArgs) {
605
- const script = typeof args.script === "string" ? args.script.trim() : "";
606
- if (!script) throw new Error("runPackageScript requires a non-empty script.");
607
- return script;
608
- }
609
-
610
546
  function requireVisualWaitSelector(args: WorkspaceFileArgs) {
611
547
  const selector = typeof args.waitSelector === "string" ? args.waitSelector.trim() : "";
612
548
  if (!selector) throw new Error("captureVisualState requires a non-empty waitSelector.");
613
549
  return selector;
614
550
  }
615
551
 
616
- function readPackageScriptArgs(args: WorkspaceFileArgs) {
617
- return Array.isArray(args.args)
618
- ? args.args.map((value) => String(value)).filter(Boolean)
619
- : [];
620
- }
621
-
622
552
  function readOptionalStringArg(value: unknown) {
623
553
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
624
554
  }
@@ -669,6 +599,51 @@ function formatAvailableScripts(scripts: string[]) {
669
599
  return scripts.length ? scripts.join(", ") : "(none)";
670
600
  }
671
601
 
602
+ async function runWorkspacePackageScript(args: {
603
+ workspaceRoot: string;
604
+ script: string;
605
+ extraArgs?: string[];
606
+ commandTimeoutMs?: number;
607
+ }): Promise<AgentRuntimeToolResult> {
608
+ const script = args.script.trim();
609
+ if (!script) throw new Error("package script name is required.");
610
+ const extraArgs = args.extraArgs ?? [];
611
+ const packageScripts = await readWorkspacePackageScripts(args.workspaceRoot);
612
+ if (!packageScripts.scripts.includes(script)) {
613
+ return {
614
+ content: [
615
+ `script not found: ${script}`,
616
+ `available scripts: ${formatAvailableScripts(packageScripts.scripts)}`,
617
+ ...(packageScripts.error ? [`package.json read error: ${packageScripts.error}`] : []),
618
+ ].join("\n"),
619
+ metadata: {
620
+ script,
621
+ exitCode: 1,
622
+ reason: "script-not-found",
623
+ availableScripts: packageScripts.scripts,
624
+ ...(packageScripts.error ? { packageJsonError: packageScripts.error } : {}),
625
+ },
626
+ };
627
+ }
628
+ const result = await runWorkspaceCommand({
629
+ workspaceRoot: args.workspaceRoot,
630
+ command: ["bun", "run", script, ...(extraArgs.length ? ["--", ...extraArgs] : [])],
631
+ timeoutMs: args.commandTimeoutMs,
632
+ });
633
+ return {
634
+ content: result.content,
635
+ metadata: {
636
+ script,
637
+ args: extraArgs,
638
+ exitCode: result.exitCode,
639
+ reason: result.exitCode === 0 ? "ok" : "script-failed",
640
+ timedOut: result.timedOut,
641
+ stdoutTail: result.stdout.trim().slice(-4000),
642
+ stderrTail: result.stderr.trim().slice(-4000),
643
+ },
644
+ };
645
+ }
646
+
672
647
  function truncateToolOutput(value: string, limit = 20_000) {
673
648
  if (value.length <= limit) return value;
674
649
  return `${value.slice(0, limit)}\n\n[truncated ${value.length - limit} chars]`;
@@ -712,9 +687,13 @@ async function runWorkspaceCommand(args: {
712
687
  command: string[];
713
688
  stdin?: string;
714
689
  timeoutMs?: number;
690
+ outputLimit?: number;
715
691
  commandPrefix?: string[];
716
692
  }) {
717
- const timeoutMs = args.timeoutMs ?? DEFAULT_WORKSPACE_COMMAND_TIMEOUT_MS;
693
+ const timeoutMs =
694
+ typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs > 0
695
+ ? args.timeoutMs
696
+ : undefined;
718
697
  const detached = process.platform !== "win32";
719
698
  const command = [
720
699
  ...(args.commandPrefix ?? []),
@@ -732,12 +711,14 @@ async function runWorkspaceCommand(args: {
732
711
  const stderrPromise = new Response(proc.stderr).text();
733
712
  let timeout: ReturnType<typeof setTimeout> | undefined;
734
713
  const timeoutResult = Symbol("timeout");
735
- const exitOrTimeout = await Promise.race([
736
- proc.exited,
737
- new Promise<typeof timeoutResult>((resolveTimeout) => {
738
- timeout = setTimeout(() => resolveTimeout(timeoutResult), timeoutMs);
739
- }),
740
- ]);
714
+ const exitOrTimeout = timeoutMs
715
+ ? await Promise.race([
716
+ proc.exited,
717
+ new Promise<typeof timeoutResult>((resolveTimeout) => {
718
+ timeout = setTimeout(() => resolveTimeout(timeoutResult), timeoutMs);
719
+ }),
720
+ ])
721
+ : await proc.exited;
741
722
  if (timeout) clearTimeout(timeout);
742
723
  const timedOut = exitOrTimeout === timeoutResult;
743
724
  if (timedOut) {
@@ -769,7 +750,7 @@ async function runWorkspaceCommand(args: {
769
750
  ]);
770
751
  const exitCode = timedOut ? 124 : Number(exitOrTimeout);
771
752
  const stderr = timedOut
772
- ? `${rawStderr.trim() ? `${rawStderr.trim()}\n` : ""}command timed out after ${timeoutMs}ms\n`
753
+ ? `${rawStderr.trim() ? `${rawStderr.trim()}\n` : ""}command timed out after ${timeoutMs ?? "unknown"}ms\n`
773
754
  : rawStderr;
774
755
  return {
775
756
  stdout,
@@ -780,7 +761,7 @@ async function runWorkspaceCommand(args: {
780
761
  stdout.trim() ? `stdout:\n${stdout.trim()}` : "",
781
762
  stderr.trim() ? `stderr:\n${stderr.trim()}` : "",
782
763
  `exitCode: ${exitCode}`,
783
- ].filter(Boolean).join("\n\n")),
764
+ ].filter(Boolean).join("\n\n"), args.outputLimit),
784
765
  };
785
766
  }
786
767
 
@@ -983,82 +964,6 @@ async function searchWorkspaceTool(args: {
983
964
  };
984
965
  }
985
966
 
986
- async function applyPatchTool(args: {
987
- call: AgentRuntimeToolCallInput;
988
- workspaceRoot: string;
989
- }): Promise<AgentRuntimeToolResult> {
990
- const parsed = parseWorkspaceToolArguments(args.call.arguments);
991
- const patch = requireWorkspacePatch(parsed);
992
- const checkResult = await runWorkspaceCommand({
993
- workspaceRoot: args.workspaceRoot,
994
- command: ["git", "apply", "--check", "--whitespace=nowarn", "-"],
995
- stdin: patch,
996
- });
997
- if (checkResult.exitCode !== 0) {
998
- throw new Error(
999
- [
1000
- "applyPatch failed during git apply --check.",
1001
- "Use editFile/applyEdit for exact single-file edits when possible.",
1002
- checkResult.content,
1003
- ].join("\n\n")
1004
- );
1005
- }
1006
- const result = await runWorkspaceCommand({
1007
- workspaceRoot: args.workspaceRoot,
1008
- command: ["git", "apply", "--whitespace=nowarn", "-"],
1009
- stdin: patch,
1010
- });
1011
- if (result.exitCode !== 0) throw new Error(result.content);
1012
- return {
1013
- content: "patch applied",
1014
- metadata: { bytes: Buffer.byteLength(patch) },
1015
- };
1016
- }
1017
-
1018
- async function runPackageScriptTool(args: {
1019
- call: AgentRuntimeToolCallInput;
1020
- workspaceRoot: string;
1021
- commandTimeoutMs?: number;
1022
- }): Promise<AgentRuntimeToolResult> {
1023
- const parsed = parseWorkspaceToolArguments(args.call.arguments);
1024
- const script = requirePackageScript(parsed);
1025
- const extraArgs = readPackageScriptArgs(parsed);
1026
- const packageScripts = await readWorkspacePackageScripts(args.workspaceRoot);
1027
- if (!packageScripts.scripts.includes(script)) {
1028
- return {
1029
- content: [
1030
- `script not found: ${script}`,
1031
- `available scripts: ${formatAvailableScripts(packageScripts.scripts)}`,
1032
- ...(packageScripts.error ? [`package.json read error: ${packageScripts.error}`] : []),
1033
- ].join("\n"),
1034
- metadata: {
1035
- script,
1036
- exitCode: 1,
1037
- reason: "script-not-found",
1038
- availableScripts: packageScripts.scripts,
1039
- ...(packageScripts.error ? { packageJsonError: packageScripts.error } : {}),
1040
- },
1041
- };
1042
- }
1043
- const result = await runWorkspaceCommand({
1044
- workspaceRoot: args.workspaceRoot,
1045
- command: ["bun", "run", script, ...(extraArgs.length ? ["--", ...extraArgs] : [])],
1046
- timeoutMs: args.commandTimeoutMs,
1047
- });
1048
- return {
1049
- content: result.content,
1050
- metadata: {
1051
- script,
1052
- args: extraArgs,
1053
- exitCode: result.exitCode,
1054
- reason: result.exitCode === 0 ? "ok" : "script-failed",
1055
- timedOut: result.timedOut,
1056
- stdoutTail: result.stdout.trim().slice(-4000),
1057
- stderrTail: result.stderr.trim().slice(-4000),
1058
- },
1059
- };
1060
- }
1061
-
1062
967
  async function resolveVisualStateBaseUrl(args: {
1063
968
  workspaceRoot: string;
1064
969
  explicitBaseUrl?: string;
@@ -1106,16 +1011,10 @@ async function captureVisualStateTool(args: {
1106
1011
  "--metrics",
1107
1012
  captureArgs.metricsPath,
1108
1013
  ];
1109
- const result = await runPackageScriptTool({
1110
- call: {
1111
- id: args.call.id,
1112
- name: "runPackageScript",
1113
- arguments: JSON.stringify({
1114
- script: "probe:visual-review",
1115
- args: extraArgs,
1116
- }),
1117
- },
1014
+ const result = await runWorkspacePackageScript({
1118
1015
  workspaceRoot: args.workspaceRoot,
1016
+ script: "probe:visual-review",
1017
+ extraArgs,
1119
1018
  commandTimeoutMs: args.commandTimeoutMs,
1120
1019
  });
1121
1020
  const metrics = parseLastJsonObject(String(result.metadata?.stdoutTail ?? result.content));
@@ -1153,13 +1052,9 @@ async function previewLifecycleTool(args: {
1153
1052
  commandTimeoutMs?: number;
1154
1053
  script: "preview:start" | "preview:status" | "preview:stop" | "preview:release";
1155
1054
  }): Promise<AgentRuntimeToolResult> {
1156
- const result = await runPackageScriptTool({
1157
- call: {
1158
- id: args.call.id,
1159
- name: "runPackageScript",
1160
- arguments: JSON.stringify({ script: args.script }),
1161
- },
1055
+ const result = await runWorkspacePackageScript({
1162
1056
  workspaceRoot: args.workspaceRoot,
1057
+ script: args.script,
1163
1058
  commandTimeoutMs: args.commandTimeoutMs,
1164
1059
  });
1165
1060
  const summary = parseLastJsonObject(String(result.metadata?.stdoutTail ?? result.content)) ?? {};
@@ -1195,6 +1090,7 @@ async function execShellTool(args: {
1195
1090
  shell: parsed.shell,
1196
1091
  }),
1197
1092
  timeoutMs: args.commandTimeoutMs,
1093
+ outputLimit: args.commandOutputLimit,
1198
1094
  commandPrefix: args.commandPrefix,
1199
1095
  });
1200
1096
  return {
@@ -1253,15 +1149,6 @@ export function createLocalWorkspaceToolExecutors(args: LocalWorkspaceToolArgs)
1253
1149
  call,
1254
1150
  workspaceRoot: args.workspaceRoot,
1255
1151
  }),
1256
- applyPatch: (call: AgentRuntimeToolCallInput) => applyPatchTool({
1257
- call,
1258
- workspaceRoot: args.workspaceRoot,
1259
- }),
1260
- runPackageScript: (call: AgentRuntimeToolCallInput) => runPackageScriptTool({
1261
- call,
1262
- workspaceRoot: args.workspaceRoot,
1263
- commandTimeoutMs: args.commandTimeoutMs,
1264
- }),
1265
1152
  startPreview: (call: AgentRuntimeToolCallInput) => previewLifecycleTool({
1266
1153
  call,
1267
1154
  workspaceRoot: args.workspaceRoot,
@@ -1295,6 +1182,7 @@ export function createLocalWorkspaceToolExecutors(args: LocalWorkspaceToolArgs)
1295
1182
  call,
1296
1183
  workspaceRoot: args.workspaceRoot,
1297
1184
  commandTimeoutMs: args.commandTimeoutMs,
1185
+ commandOutputLimit: args.commandOutputLimit,
1298
1186
  commandPrefix: args.commandPrefix,
1299
1187
  }),
1300
1188
  };
@@ -1,6 +1,8 @@
1
1
  import type { AgentRuntimeAgentConfig } from "./hostAdapter";
2
2
  import type { AgentRuntimeHost, AgentRuntimeToolPolicy } from "./types";
3
3
 
4
+ type EnvLike = Record<string, string | undefined>;
5
+
4
6
  function isRecord(value: unknown): value is Record<string, unknown> {
5
7
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
8
  }
@@ -56,6 +58,11 @@ function runtimeToolNamesFromPolicy(policy: AgentRuntimeToolPolicy | undefined)
56
58
  return stringArray(policy?.runtimeTools);
57
59
  }
58
60
 
61
+ function readPositiveNumber(value: unknown) {
62
+ const parsed = Number(value);
63
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
64
+ }
65
+
59
66
  export function mergeAgentRuntimeToolPolicies(
60
67
  base?: AgentRuntimeToolPolicy,
61
68
  override?: AgentRuntimeToolPolicy,
@@ -110,6 +117,44 @@ export function resolveRequestedRuntimeToolNames(args: {
110
117
  ]);
111
118
  }
112
119
 
120
+ export function resolveCurrentRunRuntimeToolPolicy(
121
+ agentConfig: Pick<AgentRuntimeAgentConfig, "runtimeToolPolicy" | "runtimeBinding"> | null | undefined,
122
+ ): AgentRuntimeToolPolicy | undefined {
123
+ const runtimeBinding = runtimeBindingRecord(agentConfig?.runtimeBinding);
124
+ return normalizeAgentRuntimeToolPolicy(
125
+ runtimeBinding?.runtimeToolPolicySnapshot ??
126
+ agentConfig?.runtimeToolPolicy ??
127
+ runtimeBinding?.runtimeToolPolicy,
128
+ );
129
+ }
130
+
131
+ export function resolveLocalRuntimeEnvFromPolicy(
132
+ runtimeEnv: EnvLike,
133
+ policy?: AgentRuntimeToolPolicy,
134
+ ): EnvLike {
135
+ const enablesWorktreeShell =
136
+ Array.isArray(policy?.runtimeTools) &&
137
+ policy.runtimeTools.includes("execShell") &&
138
+ policy.shell?.enabled === true &&
139
+ policy.shell?.mode === "worktree";
140
+ return {
141
+ ...runtimeEnv,
142
+ ...(policy?.workspace?.cwd ? { NOLO_LOCAL_WORKTREE: policy.workspace.cwd } : {}),
143
+ ...(enablesWorktreeShell && !runtimeEnv.NOLO_LOCAL_SHELL_MODE
144
+ ? { NOLO_LOCAL_SHELL_MODE: "worktree" }
145
+ : {}),
146
+ };
147
+ }
148
+
149
+ export function resolveLocalWorkspaceExecutorOptionsFromPolicy(
150
+ policy?: AgentRuntimeToolPolicy,
151
+ ) {
152
+ const maxOutputBytes = readPositiveNumber(policy?.shell?.maxOutputBytes);
153
+ return {
154
+ ...(maxOutputBytes ? { commandOutputLimit: Math.floor(maxOutputBytes) } : {}),
155
+ };
156
+ }
157
+
113
158
  function policyRequestsHostedWorkspace(policy: AgentRuntimeToolPolicy | undefined) {
114
159
  if (!policy) return false;
115
160
  const workspaceMode = policy.workspace?.mode;
@@ -150,12 +195,7 @@ export function resolveEffectiveRuntimeToolPolicy(args: {
150
195
  taskRunControl?: { runtimeToolPolicy?: unknown } | null;
151
196
  host?: AgentRuntimeHost;
152
197
  }): AgentRuntimeToolPolicy | undefined {
153
- const agentPolicy = normalizeAgentRuntimeToolPolicy(
154
- args.agentConfig.runtimeToolPolicy ??
155
- (isRecord(args.agentConfig.runtimeBinding)
156
- ? args.agentConfig.runtimeBinding.runtimeToolPolicy
157
- : undefined),
158
- );
198
+ const agentPolicy = resolveCurrentRunRuntimeToolPolicy(args.agentConfig);
159
199
  const taskPolicy = normalizeAgentRuntimeToolPolicy(
160
200
  args.taskRunControl?.runtimeToolPolicy,
161
201
  );
@@ -6,6 +6,7 @@ import {
6
6
  readlink,
7
7
  rm,
8
8
  symlink,
9
+ unlink,
9
10
  } from "node:fs/promises";
10
11
  import { dirname, join, resolve, sep } from "node:path";
11
12
 
@@ -73,22 +74,23 @@ export async function collectWorkspacePackageLinks(workspacePath: string) {
73
74
  () => []
74
75
  );
75
76
  const nodeModulesPath = join(workspacePath, "node_modules");
76
- const links: WorkspacePackageLink[] = [];
77
-
78
- for (const entry of packageDirs) {
79
- if (!entry.isDirectory()) continue;
80
- const target = join(packagesRoot, entry.name);
81
- const name = await readPackageName(join(target, "package.json")).catch(
82
- () => null
83
- );
84
- if (!name) continue;
85
- links.push({
86
- name,
87
- target,
88
- linkPath: packageLinkPath(nodeModulesPath, name),
77
+
78
+ const linksPromises = packageDirs
79
+ .filter((entry) => entry.isDirectory())
80
+ .map(async (entry) => {
81
+ const target = join(packagesRoot, entry.name);
82
+ const name = await readPackageName(join(target, "package.json")).catch(
83
+ () => null
84
+ );
85
+ if (!name) return null;
86
+ return {
87
+ name,
88
+ target,
89
+ linkPath: packageLinkPath(nodeModulesPath, name),
90
+ };
89
91
  });
90
- }
91
92
 
93
+ const links = (await Promise.all(linksPromises)).filter(Boolean) as WorkspacePackageLink[];
92
94
  return links;
93
95
  }
94
96
 
@@ -106,7 +108,7 @@ async function ensureWorkspacePackageLink(link: WorkspacePackageLink) {
106
108
  if (!existingTarget) {
107
109
  throw new Error(`Cannot replace non-symlink workspace package path: ${link.linkPath}`);
108
110
  }
109
- await rm(link.linkPath, { force: true });
111
+ await unlink(link.linkPath);
110
112
  }
111
113
  await symlink(link.target, link.linkPath, "dir");
112
114
  return true;
@@ -134,7 +136,7 @@ async function ensureWorkspaceNodeModulesDirectory(workspacePath: string) {
134
136
  if (symlinkTarget) {
135
137
  const resolvedTarget = resolve(dirname(nodeModulesPath), symlinkTarget);
136
138
  if (!isInsideWorkspace(workspacePath, resolvedTarget)) {
137
- await rm(nodeModulesPath, { force: true });
139
+ await unlink(nodeModulesPath);
138
140
  }
139
141
  }
140
142
  await mkdir(nodeModulesPath, { recursive: true });
@@ -150,12 +152,10 @@ export async function ensureWorkspacePackageLinks(workspacePath: string) {
150
152
  };
151
153
  }
152
154
  await ensureWorkspaceNodeModulesDirectory(workspacePath);
153
- let createdOrUpdated = 0;
154
- for (const link of links) {
155
- if (await ensureWorkspacePackageLink(link)) {
156
- createdOrUpdated += 1;
157
- }
158
- }
155
+ const results = await Promise.all(
156
+ links.map((link) => ensureWorkspacePackageLink(link))
157
+ );
158
+ const createdOrUpdated = results.filter(Boolean).length;
159
159
  return {
160
160
  workspacePath,
161
161
  links,
@@ -173,7 +173,7 @@ export async function prepareGitTaskWorkspace(args: {
173
173
  return { path: args.env.NOLO_LOCAL_WORKTREE };
174
174
  }
175
175
  const cwd = args.cwd ?? process.cwd();
176
- const root = await runGitForTaskWorkspace(["rev-parse", "--show-toplevel"], cwd);
176
+ const root = resolve(await runGitForTaskWorkspace(["rev-parse", "--show-toplevel"], cwd));
177
177
  if (isFormalTaskWorkspacePath(root)) {
178
178
  await ensureWorkspacePackageLinks(root);
179
179
  return { path: root };
@@ -88,7 +88,6 @@ export type AgentRuntimeShellPolicy = {
88
88
  mode?: "off" | "worktree";
89
89
  commandPolicy?: "denylist" | "allowlist" | "approval";
90
90
  networkPolicy?: "default-deny" | "allowed" | "approval";
91
- maxSeconds?: number;
92
91
  maxOutputBytes?: number;
93
92
  };
94
93