nolo-cli 0.1.35 → 0.1.37

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.
@@ -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";
@@ -221,6 +223,29 @@ function buildRequestTimeoutSignal(timeoutMs?: number) {
221
223
  return controller.signal;
222
224
  }
223
225
 
226
+ function summarizeEndpoint(value: unknown) {
227
+ if (typeof value !== "string" || !value.trim()) return undefined;
228
+ try {
229
+ const url = new URL(value);
230
+ return `${url.protocol}//${url.host}${url.pathname}`;
231
+ } catch {
232
+ return "invalid-url";
233
+ }
234
+ }
235
+
236
+ function parseJsonObject(raw: string) {
237
+ try {
238
+ const parsed = JSON.parse(raw);
239
+ return parsed && typeof parsed === "object" ? parsed : {};
240
+ } catch {
241
+ return {};
242
+ }
243
+ }
244
+
245
+ function logLocalRuntimeDiagnostic(event: string, fields: Record<string, unknown>) {
246
+ console.error(`[nolo-local-runtime] ${event} ${JSON.stringify(fields)}`);
247
+ }
248
+
224
249
  function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
225
250
  const toolset = buildLocalWorkspaceToolsetForEnv(args);
226
251
  return [
@@ -314,9 +339,15 @@ function buildLocalToolExecutors(args: {
314
339
  env: EnvLike;
315
340
  fetchImpl: typeof fetch;
316
341
  localToolExecutors?: CliLocalRuntimeAdapterDeps["localToolExecutors"];
342
+ commandTimeoutMs?: number;
343
+ commandOutputLimit?: number;
317
344
  }) {
318
345
  return {
319
- ...createLocalWorkspaceToolExecutors({ workspaceRoot: args.workspaceRoot }),
346
+ ...createLocalWorkspaceToolExecutors({
347
+ workspaceRoot: args.workspaceRoot,
348
+ commandTimeoutMs: args.commandTimeoutMs,
349
+ commandOutputLimit: args.commandOutputLimit,
350
+ }),
320
351
  ...buildServerPlatformToolExecutors({
321
352
  env: args.env,
322
353
  fetchImpl: args.fetchImpl,
@@ -411,11 +442,13 @@ export function createCliLocalRuntimeAdapter(
411
442
  ? { defaultCwd: deps.cwd }
412
443
  : { cwd: deps.cwd }
413
444
  );
445
+ let runtimeToolExecutionLimits: ReturnType<typeof resolveLocalWorkspaceExecutorOptionsFromPolicy> = {};
414
446
  let localToolExecutors = buildLocalToolExecutors({
415
447
  workspaceRoot: workspaceSession.workspaceRoot,
416
448
  env: deps.env,
417
449
  fetchImpl,
418
450
  localToolExecutors: deps.localToolExecutors,
451
+ ...runtimeToolExecutionLimits,
419
452
  });
420
453
 
421
454
  async function activateAgentWorkspaceSession(args: {
@@ -435,6 +468,7 @@ export function createCliLocalRuntimeAdapter(
435
468
  env: deps.env,
436
469
  fetchImpl,
437
470
  localToolExecutors: deps.localToolExecutors,
471
+ ...runtimeToolExecutionLimits,
438
472
  });
439
473
  deps.output?.write(formatWorkspaceSessionActivation(workspaceSession));
440
474
  }
@@ -455,7 +489,17 @@ export function createCliLocalRuntimeAdapter(
455
489
  toolNames: requestedToolNames,
456
490
  env: deps.env,
457
491
  });
492
+ runtimeToolExecutionLimits = resolveLocalWorkspaceExecutorOptionsFromPolicy(
493
+ resolveCurrentRunRuntimeToolPolicy(agentConfig),
494
+ );
458
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
+ });
459
503
  return agentConfig;
460
504
  },
461
505
  loadDialogHistory: async (dialogId) => readDialogMessages({
@@ -476,6 +520,20 @@ export function createCliLocalRuntimeAdapter(
476
520
  agentConfig,
477
521
  env: deps.env,
478
522
  });
523
+ logLocalRuntimeDiagnostic("provider.selected", {
524
+ agentKey: agentConfig.key,
525
+ transport: "platform-proxy",
526
+ apiSource: agentConfig.apiSource ?? null,
527
+ provider: providerConfig.provider,
528
+ model: providerConfig.model,
529
+ endpoint: summarizeEndpoint(providerConfig.endpoint) ?? null,
530
+ proxyServer: summarizeEndpoint(providerConfig.serverUrl) ?? null,
531
+ hasAuthToken: Boolean(providerConfig.authToken),
532
+ hasApiKey: Boolean(providerConfig.apiKey),
533
+ apiKeyHeader: providerConfig.apiKeyHeader ?? null,
534
+ useServerProxy: agentConfig.useServerProxy ?? null,
535
+ customProviderEndpoint: summarizeEndpoint(agentConfig.customProviderUrl) ?? null,
536
+ });
479
537
  return {
480
538
  model: providerConfig.model,
481
539
  complete: async (messages, options) => {
@@ -492,6 +550,16 @@ export function createCliLocalRuntimeAdapter(
492
550
  tools,
493
551
  });
494
552
  const signal = buildRequestTimeoutSignal(options?.timeoutMs);
553
+ logLocalRuntimeDiagnostic("provider.request.start", {
554
+ agentKey: agentConfig.key,
555
+ transport: "platform-proxy",
556
+ requestUrl: summarizeEndpoint(request.url) ?? null,
557
+ endpoint: summarizeEndpoint(providerConfig.endpoint) ?? null,
558
+ model: providerConfig.model,
559
+ messageCount: messages.length,
560
+ toolCount: tools.length,
561
+ timeoutMs: options?.timeoutMs ?? null,
562
+ });
495
563
  const res = await fetchWithTransientRetry(fetchImpl, request.url, {
496
564
  ...request.init,
497
565
  ...(signal ? { signal } : {}),
@@ -499,7 +567,15 @@ export function createCliLocalRuntimeAdapter(
499
567
  sleep: deps.sleep,
500
568
  loopbackRequest,
501
569
  });
502
- const data = parsePlatformChatCompletionData(await res.text().catch(() => ""));
570
+ const raw = await res.text().catch(() => "");
571
+ logLocalRuntimeDiagnostic("provider.request.result", {
572
+ agentKey: agentConfig.key,
573
+ transport: "platform-proxy",
574
+ status: res.status,
575
+ ok: res.ok,
576
+ responseBytes: raw.length,
577
+ });
578
+ const data = parsePlatformChatCompletionData(raw);
503
579
  if (!res.ok) {
504
580
  throw new Error(`platform provider failed: HTTP ${res.status} ${JSON.stringify(data)}`);
505
581
  }
@@ -516,6 +592,18 @@ export function createCliLocalRuntimeAdapter(
516
592
  agentConfig,
517
593
  env: deps.env,
518
594
  });
595
+ logLocalRuntimeDiagnostic("provider.selected", {
596
+ agentKey: agentConfig.key,
597
+ transport: "direct-openai-compatible",
598
+ apiSource: agentConfig.apiSource ?? null,
599
+ provider: providerConfig.provider,
600
+ model: providerConfig.model,
601
+ endpoint: summarizeEndpoint(providerConfig.endpoint) ?? null,
602
+ hasApiKey: Boolean(providerConfig.apiKey),
603
+ apiKeyHeader: providerConfig.apiKeyHeader ?? null,
604
+ useServerProxy: agentConfig.useServerProxy ?? null,
605
+ customProviderEndpoint: summarizeEndpoint(agentConfig.customProviderUrl) ?? null,
606
+ });
519
607
  return {
520
608
  model: providerConfig.model,
521
609
  complete: async (messages, options) => {
@@ -532,6 +620,15 @@ export function createCliLocalRuntimeAdapter(
532
620
  tools,
533
621
  });
534
622
  const signal = buildRequestTimeoutSignal(options?.timeoutMs);
623
+ logLocalRuntimeDiagnostic("provider.request.start", {
624
+ agentKey: agentConfig.key,
625
+ transport: "direct-openai-compatible",
626
+ requestUrl: summarizeEndpoint(request.url) ?? null,
627
+ model: providerConfig.model,
628
+ messageCount: messages.length,
629
+ toolCount: tools.length,
630
+ timeoutMs: options?.timeoutMs ?? null,
631
+ });
535
632
  const res = await fetchWithTransientRetry(fetchImpl, request.url, {
536
633
  ...request.init,
537
634
  ...(signal ? { signal } : {}),
@@ -539,7 +636,15 @@ export function createCliLocalRuntimeAdapter(
539
636
  sleep: deps.sleep,
540
637
  loopbackRequest,
541
638
  });
542
- const data = await res.json().catch(() => ({}));
639
+ const raw = await res.text().catch(() => "");
640
+ logLocalRuntimeDiagnostic("provider.request.result", {
641
+ agentKey: agentConfig.key,
642
+ transport: "direct-openai-compatible",
643
+ status: res.status,
644
+ ok: res.ok,
645
+ responseBytes: raw.length,
646
+ });
647
+ const data = parseJsonObject(raw);
543
648
  if (!res.ok) {
544
649
  throw new Error(`local provider failed: HTTP ${res.status} ${JSON.stringify(data)}`);
545
650
  }
@@ -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,
@@ -96,6 +97,37 @@ function logConnectorRun(message: string, fields: Record<string, unknown> = {})
96
97
  console.error(`[nolo-connector] ${message} ${JSON.stringify(fields)}`);
97
98
  }
98
99
 
100
+ function summarizeEndpoint(value: unknown) {
101
+ if (typeof value !== "string" || !value.trim()) return undefined;
102
+ try {
103
+ const url = new URL(value);
104
+ return `${url.protocol}//${url.host}${url.pathname}`;
105
+ } catch {
106
+ return "invalid-url";
107
+ }
108
+ }
109
+
110
+ function summarizeAgentConfigForLogs(agentConfig: any) {
111
+ const runtimeBinding = isRecord(agentConfig?.runtimeBinding) ? agentConfig.runtimeBinding : {};
112
+ return {
113
+ apiSource: agentConfig?.apiSource ?? null,
114
+ provider: agentConfig?.provider ?? null,
115
+ cliProvider: agentConfig?.cliProvider ?? null,
116
+ model: agentConfig?.model ?? null,
117
+ customProviderEndpoint: summarizeEndpoint(agentConfig?.customProviderUrl) ?? null,
118
+ useServerProxy: agentConfig?.useServerProxy ?? null,
119
+ skipLocalRuntimeEnsure: agentConfig?.skipLocalRuntimeEnsure ?? null,
120
+ hasApiKey: typeof agentConfig?.apiKey === "string" && agentConfig.apiKey.length > 0,
121
+ apiKeyHeader: agentConfig?.apiKeyHeader ?? null,
122
+ hasApiKeyFromAgentKey:
123
+ typeof agentConfig?.apiKeyFromAgentKey === "string" &&
124
+ agentConfig.apiKeyFromAgentKey.length > 0,
125
+ runtimeBindingKind: runtimeBinding.kind ?? null,
126
+ runtimeMachineId: runtimeBinding.machineId ?? null,
127
+ connectorSurface: runtimeBinding.connectorSurface ?? runtimeBinding.surface ?? null,
128
+ };
129
+ }
130
+
99
131
  function isRecord(value: unknown): value is Record<string, any> {
100
132
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
101
133
  }
@@ -194,11 +226,8 @@ function mergeToolNames(...values: unknown[]) {
194
226
  return names;
195
227
  }
196
228
 
197
- function localRuntimeEnvFromPolicy(runtimeEnv: EnvLike, policy?: ConnectorRuntimePolicy): EnvLike {
198
- return {
199
- ...runtimeEnv,
200
- ...(policy?.workspace?.cwd ? { NOLO_LOCAL_WORKTREE: policy.workspace.cwd } : {}),
201
- };
229
+ export function localRuntimeEnvFromPolicy(runtimeEnv: EnvLike, policy?: ConnectorRuntimePolicy): EnvLike {
230
+ return resolveLocalRuntimeEnvFromPolicy(runtimeEnv, policy);
202
231
  }
203
232
 
204
233
  function forwardedUserAuthToken(parsed: any): string {
@@ -569,6 +598,15 @@ export async function handleConnectorRunMessage(
569
598
  if (!requestsLocalWorkspaceRuntime(runtimePolicy) && !isMachineBoundLocalCustomProvider(agentConfig)) {
570
599
  throw new Error("Connector can only execute non-CLI agents when runtimeToolPolicySnapshot requests a local workspace runtime.");
571
600
  }
601
+ logConnectorRun("agent.run.local-runtime.start", {
602
+ requestId: parsed.requestId,
603
+ agentKey: parsed.payload?.agentKey,
604
+ cwd,
605
+ timeout,
606
+ machineBoundLocalCustomProvider: isMachineBoundLocalCustomProvider(agentConfig),
607
+ runtimePolicy: Boolean(runtimePolicy),
608
+ ...summarizeAgentConfigForLogs(agentConfig),
609
+ });
572
610
  baseSha = await readConnectorGitHead(cwd);
573
611
  const result = await runConnectorLocalRuntimeAgent({
574
612
  parsed,
@@ -607,6 +645,8 @@ export async function handleConnectorRunMessage(
607
645
  logConnectorRun("agent.run.error", {
608
646
  requestId: parsed.requestId,
609
647
  agentKey: parsed.payload?.agentKey,
648
+ ...summarizeAgentConfigForLogs(agentConfig),
649
+ errorName: error instanceof Error ? error.name : null,
610
650
  error: error instanceof Error ? error.message : String(error),
611
651
  });
612
652
  send(JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {
@@ -158,8 +158,9 @@
158
158
  "agent-runtime/runtimeDecision.ts",
159
159
  "agent-runtime/workspaceSession.ts",
160
160
  "agent-runtime/taskWorkspace.ts",
161
- "agent-runtime/types.ts",
161
+ "agent-runtime/runtimeToolPolicy.ts",
162
162
  "agent-runtime/hostAdapter.ts",
163
+ "agent-runtime/types.ts",
163
164
  "agent-runtime/agentConfigOptions.ts",
164
165
  "agent-runtime/openAiCompatibleProvider.ts",
165
166
  "agent-runtime/openAiCompatibleProviderConfig.ts",
@@ -173,7 +174,6 @@
173
174
  "agent-runtime/hybridRecordStore.ts",
174
175
  "agent-runtime/localToolPolicy.ts",
175
176
  "agent-runtime/shellCommandPolicy.ts",
176
- "agent-runtime/runtimeToolPolicy.ts",
177
177
  "connector-experimental/heartbeatLoop.ts",
178
178
  "connector-experimental/capabilities.ts",
179
179
  "connector-experimental/codexBinary.ts",