nolo-cli 0.1.39 → 0.1.40
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 +4 -4
- package/agent-runtime/dialogWritePlan.ts +39 -0
- package/agent-runtime/hostAdapter.ts +4 -0
- package/agent-runtime/hybridRecordStore.ts +1 -1
- package/agent-runtime/index.ts +15 -0
- package/agent-runtime/localLoop.ts +87 -7
- package/agent-runtime/localToolPolicy.ts +2 -13
- package/agent-runtime/localWorkspaceTools.ts +42 -110
- package/agent-runtime/runtimeToolPolicy.ts +2 -11
- package/agent-runtime/runtimeToolSurface.ts +198 -0
- package/agent-runtime/types.ts +1 -0
- package/agentAliases.ts +2 -1
- package/agentRunCommand.ts +10 -32
- package/agentRuntimeLocal.ts +1 -0
- package/ai/agent/cliExecutor.ts +249 -34
- package/cli/agentAliases.ts +2 -1
- package/cli/agentRunCommand.ts +10 -32
- package/cli/agentRuntimeLocal.ts +1 -0
- package/cli/client/agentRun.ts +40 -87
- package/cli/client/localRuntimeAdapter.ts +179 -26
- package/cli/commandRegistry.ts +2 -2
- package/cli/machineWsRunDispatch.ts +51 -6
- package/cli/offlineMarxistsAgentCommand.ts +6 -4
- package/client/agentConfigResolver.test.ts +2 -2
- package/client/agentRun.test.ts +123 -57
- package/client/agentRun.ts +40 -87
- package/client/localRuntimeAdapter.test.ts +245 -31
- package/client/localRuntimeAdapter.ts +179 -26
- package/client/localRuntimeDryRun.test.ts +23 -9
- package/client/localToolPolicy.test.ts +4 -4
- package/commandRegistry.ts +2 -2
- package/database/server/levelAuthorityStore.ts +20 -1
- package/machineWsRunDispatch.ts +51 -6
- package/offlineMarxistsAgentCommand.ts +6 -4
- package/package.json +2 -1
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
resolveCurrentRunRuntimeToolPolicy,
|
|
22
22
|
resolveLocalWorkspaceExecutorOptionsFromPolicy,
|
|
23
23
|
resolveRequestedRuntimeToolNames,
|
|
24
|
+
resolveRuntimeToolSurfaceForAgent,
|
|
24
25
|
shouldUsePlatformChatProvider,
|
|
25
26
|
} from "../agentRuntimeLocal";
|
|
26
27
|
import type { AgentRuntimeChatMessage } from "../agent-runtime";
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
import { executeLocalToolWithPolicy } from "./localToolPolicy";
|
|
44
45
|
import { taskRunFunctionSchema } from "../ai/tools/agent/taskRunTool";
|
|
45
46
|
import {
|
|
47
|
+
MIMO_MONTH_AGENT_KEY,
|
|
46
48
|
NOLO_DEFAULT_AGENT_ID,
|
|
47
49
|
NOLO_DEFAULT_AGENT_KEY,
|
|
48
50
|
} from "../agentAliases";
|
|
@@ -50,14 +52,26 @@ import {
|
|
|
50
52
|
buildNoloWorkspaceCliToolExecutors,
|
|
51
53
|
buildNoloWorkspaceOpenAiTools,
|
|
52
54
|
filterNoloWorkspaceToolNames,
|
|
53
|
-
NOLO_WORKSPACE_TOOL_NAMES,
|
|
54
|
-
NOLO_WORKSPACE_TOOL_PROMPT,
|
|
55
55
|
parseNoloWorkspaceToolArguments,
|
|
56
56
|
} from "../agent-runtime/noloWorkspaceTools";
|
|
57
|
+
import { executeCli as defaultExecuteCli, type CliExecuteResult, type CliImageInput } from "../ai/agent/cliExecutor";
|
|
58
|
+
import { buildCliPrompt } from "../ai/agent/cliPrompt";
|
|
57
59
|
|
|
58
60
|
type EnvLike = Record<string, string | undefined>;
|
|
59
61
|
type FetchInput = Parameters<typeof fetch>[0];
|
|
60
62
|
type FetchInit = Parameters<typeof fetch>[1];
|
|
63
|
+
type LocalCliExecutor = (
|
|
64
|
+
provider: string,
|
|
65
|
+
prompt: string,
|
|
66
|
+
options: {
|
|
67
|
+
model?: string;
|
|
68
|
+
timeout?: number;
|
|
69
|
+
cwd?: string;
|
|
70
|
+
yolo?: boolean;
|
|
71
|
+
env?: Record<string, string | undefined>;
|
|
72
|
+
imageInputs?: CliImageInput[];
|
|
73
|
+
}
|
|
74
|
+
) => Promise<CliExecuteResult>;
|
|
61
75
|
const TRANSIENT_FETCH_MAX_ATTEMPTS = 8;
|
|
62
76
|
const TRANSIENT_FETCH_RETRY_BASE_DELAY_MS = 250;
|
|
63
77
|
export const BUILTIN_NOLO_AGENT_ID = NOLO_DEFAULT_AGENT_ID;
|
|
@@ -77,6 +91,7 @@ export type CliLocalRuntimeAdapterDeps = {
|
|
|
77
91
|
cwd?: string;
|
|
78
92
|
output?: { write(chunk: string): unknown };
|
|
79
93
|
localToolExecutors?: Record<string, (call: any) => Promise<{ content: string; metadata?: Record<string, unknown> }>>;
|
|
94
|
+
executeCli?: LocalCliExecutor;
|
|
80
95
|
sleep?: (ms: number) => Promise<void>;
|
|
81
96
|
loopbackRequest?: (input: FetchInput, init?: FetchInit) => Promise<Response>;
|
|
82
97
|
};
|
|
@@ -93,10 +108,6 @@ function resolveLocalUserId(env: EnvLike) {
|
|
|
93
108
|
return env.NOLO_LOCAL_USER_ID || env.NOLO_USER_ID || "local";
|
|
94
109
|
}
|
|
95
110
|
|
|
96
|
-
function shellModeEnabled(env: EnvLike) {
|
|
97
|
-
return env.NOLO_LOCAL_SHELL_MODE === "worktree";
|
|
98
|
-
}
|
|
99
|
-
|
|
100
111
|
function parseLocalToolBudgets(env: EnvLike) {
|
|
101
112
|
const raw = env.NOLO_LOCAL_TOOL_BUDGETS?.trim();
|
|
102
113
|
if (!raw) return {};
|
|
@@ -251,7 +262,79 @@ function parseJsonObject(raw: string) {
|
|
|
251
262
|
}
|
|
252
263
|
}
|
|
253
264
|
|
|
265
|
+
function isCliProviderAgent(agentConfig: AgentRuntimeAgentConfig) {
|
|
266
|
+
return Boolean(
|
|
267
|
+
agentConfig.apiSource === "cli" ||
|
|
268
|
+
agentConfig.provider === "cli" ||
|
|
269
|
+
agentConfig.cliProvider
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function resolveCliProviderName(agentConfig: AgentRuntimeAgentConfig) {
|
|
274
|
+
return (agentConfig.cliProvider || agentConfig.provider || "codex").trim() || "codex";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function stringifyRuntimeMessageContent(content: AgentRuntimeChatMessage["content"]) {
|
|
278
|
+
if (typeof content === "string") return content;
|
|
279
|
+
if (Array.isArray(content)) {
|
|
280
|
+
return content
|
|
281
|
+
.map((part) => {
|
|
282
|
+
if (typeof part === "string") return part;
|
|
283
|
+
if (part && typeof part === "object" && "text" in part) {
|
|
284
|
+
return String((part as any).text ?? "");
|
|
285
|
+
}
|
|
286
|
+
return JSON.stringify(part);
|
|
287
|
+
})
|
|
288
|
+
.filter((part) => part.trim())
|
|
289
|
+
.join("\n");
|
|
290
|
+
}
|
|
291
|
+
return content == null ? "" : String(content);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildPromptForCliProvider(messages: AgentRuntimeChatMessage[]) {
|
|
295
|
+
const systemPrompt = messages
|
|
296
|
+
.filter((message) => message.role === "system")
|
|
297
|
+
.map((message) => stringifyRuntimeMessageContent(message.content).trim())
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.join("\n\n");
|
|
300
|
+
const taskPrompt = messages
|
|
301
|
+
.filter((message) => message.role !== "system")
|
|
302
|
+
.map((message) => {
|
|
303
|
+
const content = stringifyRuntimeMessageContent(message.content).trim();
|
|
304
|
+
if (!content) return "";
|
|
305
|
+
return `[${message.role}]\n${content}`;
|
|
306
|
+
})
|
|
307
|
+
.filter(Boolean)
|
|
308
|
+
.join("\n\n");
|
|
309
|
+
return buildCliPrompt(systemPrompt, taskPrompt);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function collectCliProviderImageInputs(messages: AgentRuntimeChatMessage[]) {
|
|
313
|
+
const urls: string[] = [];
|
|
314
|
+
for (const message of messages) {
|
|
315
|
+
if (!Array.isArray(message.content)) continue;
|
|
316
|
+
for (const part of message.content) {
|
|
317
|
+
if (
|
|
318
|
+
part &&
|
|
319
|
+
typeof part === "object" &&
|
|
320
|
+
(part as any).type === "image_url" &&
|
|
321
|
+
typeof (part as any).image_url?.url === "string" &&
|
|
322
|
+
(part as any).image_url.url.trim()
|
|
323
|
+
) {
|
|
324
|
+
urls.push((part as any).image_url.url.trim());
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return urls;
|
|
329
|
+
}
|
|
330
|
+
|
|
254
331
|
function logLocalRuntimeDiagnostic(event: string, fields: Record<string, unknown>) {
|
|
332
|
+
if (
|
|
333
|
+
process.env.NOLO_LOCAL_RUNTIME_DEBUG !== "1" &&
|
|
334
|
+
process.env.NOLO_DEBUG !== "1"
|
|
335
|
+
) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
255
338
|
console.error(`[nolo-local-runtime] ${event} ${JSON.stringify(fields)}`);
|
|
256
339
|
}
|
|
257
340
|
|
|
@@ -266,31 +349,38 @@ function summarizeOpenAiToolNames(tools: Array<Record<string, unknown>>) {
|
|
|
266
349
|
.filter((name): name is string => Boolean(name));
|
|
267
350
|
}
|
|
268
351
|
|
|
269
|
-
function
|
|
352
|
+
function shouldExposeLocalPlatformTools(agentKey?: string) {
|
|
353
|
+
return agentKey !== MIMO_MONTH_AGENT_KEY;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function buildOpenAiTools(args: { agentKey?: string; toolNames?: string[]; env: EnvLike }) {
|
|
270
357
|
const toolset = buildLocalWorkspaceToolsetForEnv(args);
|
|
358
|
+
const exposePlatformTools = shouldExposeLocalPlatformTools(args.agentKey);
|
|
271
359
|
return [
|
|
272
360
|
...buildLocalWorkspaceOpenAiTools({
|
|
273
361
|
toolNames: toolset.toolNames,
|
|
274
362
|
exposeShellTools: toolset.exposeShellTools,
|
|
275
363
|
}),
|
|
276
|
-
...buildServerPlatformOpenAiTools({ toolNames: args.toolNames }),
|
|
277
|
-
...buildNoloWorkspaceOpenAiTools({ toolNames: args.toolNames }),
|
|
364
|
+
...(exposePlatformTools ? buildServerPlatformOpenAiTools({ toolNames: args.toolNames }) : []),
|
|
365
|
+
...(exposePlatformTools ? buildNoloWorkspaceOpenAiTools({ toolNames: args.toolNames }) : []),
|
|
278
366
|
];
|
|
279
367
|
}
|
|
280
368
|
|
|
281
369
|
function buildLocalWorkspaceToolsetForEnv(args: { toolNames?: string[]; env: EnvLike }) {
|
|
370
|
+
void args.env;
|
|
282
371
|
const toolset = buildLocalWorkspaceToolset({
|
|
283
372
|
declaredToolNames: args.toolNames,
|
|
284
|
-
exposeShellTools:
|
|
373
|
+
exposeShellTools: true,
|
|
285
374
|
});
|
|
286
375
|
return toolset;
|
|
287
376
|
}
|
|
288
377
|
|
|
289
378
|
function buildLocalPolicyToolNames(args: { toolNames?: string[]; env: EnvLike }) {
|
|
379
|
+
void args.env;
|
|
290
380
|
return [
|
|
291
381
|
...buildLocalWorkspacePolicyToolNames({
|
|
292
382
|
declaredToolNames: args.toolNames,
|
|
293
|
-
exposeShellTools:
|
|
383
|
+
exposeShellTools: true,
|
|
294
384
|
}),
|
|
295
385
|
...((args.toolNames ?? []).includes("taskRun") ? ["taskRun"] : []),
|
|
296
386
|
...filterNoloWorkspaceToolNames(args.toolNames),
|
|
@@ -336,16 +426,32 @@ function isBuiltinNoloAgentConfig(agentConfig: AgentRuntimeAgentConfig | null |
|
|
|
336
426
|
return isBuiltinNoloAgentRef(key) || isBuiltinNoloAgentRef(id);
|
|
337
427
|
}
|
|
338
428
|
|
|
339
|
-
function
|
|
340
|
-
|
|
341
|
-
|
|
429
|
+
function withResolvedRuntimeToolSurface(
|
|
430
|
+
agentConfig: AgentRuntimeAgentConfig | null,
|
|
431
|
+
env: EnvLike
|
|
432
|
+
) {
|
|
433
|
+
if (!agentConfig) return agentConfig;
|
|
434
|
+
const currentUserId = resolveLocalUserId(env);
|
|
435
|
+
const rawRecord = (agentConfig as any).rawRecord ?? {};
|
|
436
|
+
const ownerId =
|
|
437
|
+
typeof rawRecord.userId === "string" && rawRecord.userId.trim()
|
|
438
|
+
? rawRecord.userId.trim()
|
|
439
|
+
: null;
|
|
440
|
+
const toolSurface = resolveRuntimeToolSurfaceForAgent({
|
|
441
|
+
explicitToolNames: agentConfig.toolNames,
|
|
442
|
+
currentUserId,
|
|
443
|
+
agentOwnerId: ownerId,
|
|
444
|
+
agentKey: rawRecord.dbKey ?? agentConfig.key,
|
|
445
|
+
isPublic: !isBuiltinNoloAgentConfig(agentConfig) && rawRecord.isPublic === true,
|
|
446
|
+
sharingLevel: typeof rawRecord.sharingLevel === "string" ? rawRecord.sharingLevel : null,
|
|
447
|
+
trustedPrivateInvocation: isBuiltinNoloAgentConfig(agentConfig),
|
|
448
|
+
runtimeHost: "cli",
|
|
449
|
+
});
|
|
342
450
|
return {
|
|
343
451
|
...agentConfig,
|
|
344
|
-
toolNames,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
NOLO_WORKSPACE_TOOL_PROMPT,
|
|
348
|
-
].filter(Boolean).join("\n\n"),
|
|
452
|
+
toolNames: toolSurface.finalToolNames,
|
|
453
|
+
toolSurface,
|
|
454
|
+
prompt: agentConfig.prompt,
|
|
349
455
|
};
|
|
350
456
|
}
|
|
351
457
|
|
|
@@ -500,11 +606,14 @@ export function createCliLocalRuntimeAdapter(
|
|
|
500
606
|
host: "cli",
|
|
501
607
|
capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence", "local-tools"],
|
|
502
608
|
loadAgentConfig: async (agentRef) => {
|
|
503
|
-
const agentConfig =
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
609
|
+
const agentConfig = withResolvedRuntimeToolSurface(
|
|
610
|
+
await readAgentFromStore({
|
|
611
|
+
agentRef,
|
|
612
|
+
store: await resolveStore(deps),
|
|
613
|
+
userId,
|
|
614
|
+
}),
|
|
615
|
+
deps.env
|
|
616
|
+
);
|
|
508
617
|
const requestedToolNames = agentConfig
|
|
509
618
|
? resolveRequestedRuntimeToolNames({ agentConfig })
|
|
510
619
|
: [];
|
|
@@ -537,6 +646,50 @@ export function createCliLocalRuntimeAdapter(
|
|
|
537
646
|
cwd: workspaceRoot,
|
|
538
647
|
}),
|
|
539
648
|
resolveProvider: async (agentConfig) => {
|
|
649
|
+
if (isCliProviderAgent(agentConfig)) {
|
|
650
|
+
const provider = resolveCliProviderName(agentConfig);
|
|
651
|
+
logLocalRuntimeDiagnostic("provider.selected", {
|
|
652
|
+
agentKey: agentConfig.key,
|
|
653
|
+
transport: "local-cli",
|
|
654
|
+
apiSource: agentConfig.apiSource ?? null,
|
|
655
|
+
provider,
|
|
656
|
+
model: agentConfig.model ?? null,
|
|
657
|
+
cwd: workspaceRoot,
|
|
658
|
+
});
|
|
659
|
+
return {
|
|
660
|
+
model: agentConfig.model || provider,
|
|
661
|
+
complete: async (messages, options) => {
|
|
662
|
+
const executeCli = deps.executeCli ?? (defaultExecuteCli as LocalCliExecutor);
|
|
663
|
+
const imageUrls = collectCliProviderImageInputs(messages);
|
|
664
|
+
const imageInputs: CliImageInput[] | undefined =
|
|
665
|
+
imageUrls.length > 0
|
|
666
|
+
? imageUrls.map((url) => ({ source: url }))
|
|
667
|
+
: undefined;
|
|
668
|
+
const prompt = buildPromptForCliProvider(messages);
|
|
669
|
+
try {
|
|
670
|
+
const result = await executeCli(provider, prompt, {
|
|
671
|
+
...(agentConfig.model ? { model: agentConfig.model } : {}),
|
|
672
|
+
...(options?.timeoutMs ? { timeout: options.timeoutMs } : {}),
|
|
673
|
+
cwd: workspaceRoot,
|
|
674
|
+
yolo: true,
|
|
675
|
+
env: deps.env,
|
|
676
|
+
...(imageInputs ? { imageInputs } : {}),
|
|
677
|
+
});
|
|
678
|
+
return {
|
|
679
|
+
content: result.text,
|
|
680
|
+
model: agentConfig.model || provider,
|
|
681
|
+
raw: result.raw,
|
|
682
|
+
};
|
|
683
|
+
} catch (error) {
|
|
684
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Local CLI provider "${provider}" is unavailable or failed: ${message}`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
540
693
|
if (shouldUsePlatformChatProvider(deps.env, agentConfig)) {
|
|
541
694
|
const providerConfig = resolvePlatformChatProviderConfig({
|
|
542
695
|
agentConfig,
|
|
@@ -563,6 +716,7 @@ export function createCliLocalRuntimeAdapter(
|
|
|
563
716
|
agentConfig,
|
|
564
717
|
});
|
|
565
718
|
const tools = buildOpenAiTools({
|
|
719
|
+
agentKey: agentConfig.key,
|
|
566
720
|
toolNames: requestedToolNames,
|
|
567
721
|
env: deps.env,
|
|
568
722
|
});
|
|
@@ -582,7 +736,6 @@ export function createCliLocalRuntimeAdapter(
|
|
|
582
736
|
toolCount: tools.length,
|
|
583
737
|
requestedToolNames,
|
|
584
738
|
openAiToolNames: summarizeOpenAiToolNames(tools),
|
|
585
|
-
shellMode: deps.env.NOLO_LOCAL_SHELL_MODE ?? null,
|
|
586
739
|
timeoutMs: options?.timeoutMs ?? null,
|
|
587
740
|
});
|
|
588
741
|
const res = await fetchWithTransientRetry(fetchImpl, request.url, {
|
|
@@ -636,6 +789,7 @@ export function createCliLocalRuntimeAdapter(
|
|
|
636
789
|
agentConfig,
|
|
637
790
|
});
|
|
638
791
|
const tools = buildOpenAiTools({
|
|
792
|
+
agentKey: agentConfig.key,
|
|
639
793
|
toolNames: requestedToolNames,
|
|
640
794
|
env: deps.env,
|
|
641
795
|
});
|
|
@@ -654,7 +808,6 @@ export function createCliLocalRuntimeAdapter(
|
|
|
654
808
|
toolCount: tools.length,
|
|
655
809
|
requestedToolNames,
|
|
656
810
|
openAiToolNames: summarizeOpenAiToolNames(tools),
|
|
657
|
-
shellMode: deps.env.NOLO_LOCAL_SHELL_MODE ?? null,
|
|
658
811
|
timeoutMs: options?.timeoutMs ?? null,
|
|
659
812
|
});
|
|
660
813
|
const res = await fetchWithTransientRetry(fetchImpl, request.url, {
|
|
@@ -7,6 +7,26 @@ import { runLocalAgentTurn } from "../agent-runtime/localLoop";
|
|
|
7
7
|
import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
|
|
8
8
|
|
|
9
9
|
describe("CLI local runtime dry run", () => {
|
|
10
|
+
const DEFAULT_PRIVATE_LOCAL_TOOL_NAMES = [
|
|
11
|
+
"listFiles",
|
|
12
|
+
"readFile",
|
|
13
|
+
"writeFile",
|
|
14
|
+
"editFile",
|
|
15
|
+
"searchFiles",
|
|
16
|
+
"execShell",
|
|
17
|
+
"listDialogs",
|
|
18
|
+
"readDialog",
|
|
19
|
+
"listAgents",
|
|
20
|
+
"readAgent",
|
|
21
|
+
"listSpaces",
|
|
22
|
+
"readSpace",
|
|
23
|
+
"readDoc",
|
|
24
|
+
"readSkillDoc",
|
|
25
|
+
"queryTableRows",
|
|
26
|
+
"cliWhoami",
|
|
27
|
+
"cliDoctor",
|
|
28
|
+
];
|
|
29
|
+
|
|
10
30
|
test("lets a declared workspace file tool write a file and save the tool trace", async () => {
|
|
11
31
|
const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-local-runtime-dry-run-"));
|
|
12
32
|
try {
|
|
@@ -18,7 +38,7 @@ describe("CLI local runtime dry run", () => {
|
|
|
18
38
|
prompt: "Use workspace file tools to edit files.",
|
|
19
39
|
model: "qwen-coder",
|
|
20
40
|
tools: [
|
|
21
|
-
{ type: "function", function: { name: "
|
|
41
|
+
{ type: "function", function: { name: "writeFile" } },
|
|
22
42
|
],
|
|
23
43
|
}],
|
|
24
44
|
]);
|
|
@@ -52,13 +72,7 @@ describe("CLI local runtime dry run", () => {
|
|
|
52
72
|
const body = JSON.parse(String(init?.body));
|
|
53
73
|
completeCount += 1;
|
|
54
74
|
if (completeCount === 1) {
|
|
55
|
-
expect(body.tools.map((tool: any) => tool.function.name)).toEqual(
|
|
56
|
-
"writeWorkspaceFile",
|
|
57
|
-
"listFiles",
|
|
58
|
-
"readFile",
|
|
59
|
-
"editFile",
|
|
60
|
-
"searchFiles",
|
|
61
|
-
]);
|
|
75
|
+
expect(body.tools.map((tool: any) => tool.function.name)).toEqual(DEFAULT_PRIVATE_LOCAL_TOOL_NAMES);
|
|
62
76
|
return Response.json({
|
|
63
77
|
model: "qwen-coder",
|
|
64
78
|
choices: [{
|
|
@@ -68,7 +82,7 @@ describe("CLI local runtime dry run", () => {
|
|
|
68
82
|
id: "call-1",
|
|
69
83
|
type: "function",
|
|
70
84
|
function: {
|
|
71
|
-
name: "
|
|
85
|
+
name: "writeFile",
|
|
72
86
|
arguments: JSON.stringify({
|
|
73
87
|
path: "src/notification.css",
|
|
74
88
|
content: ".notification { border-radius: 8px; }\n",
|
|
@@ -3,20 +3,20 @@ import { describe, expect, test } from "bun:test";
|
|
|
3
3
|
import { executeLocalToolWithPolicy, resolveLocalToolPolicy } from "./localToolPolicy";
|
|
4
4
|
|
|
5
5
|
describe("CLI local tool policy", () => {
|
|
6
|
-
test("allows execShell
|
|
6
|
+
test("allows execShell by default", () => {
|
|
7
7
|
expect(resolveLocalToolPolicy({
|
|
8
|
-
env: {
|
|
8
|
+
env: {},
|
|
9
9
|
agentToolNames: [],
|
|
10
10
|
toolName: "execShell",
|
|
11
11
|
})).toEqual({ allowed: true, toolName: "execShell" });
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
test("
|
|
14
|
+
test("allows execShell even without env allowlist", () => {
|
|
15
15
|
expect(resolveLocalToolPolicy({
|
|
16
16
|
env: { NOLO_LOCAL_ALLOWED_TOOLS: "execShell" },
|
|
17
17
|
agentToolNames: ["execShell"],
|
|
18
18
|
toolName: "execShell",
|
|
19
|
-
})).
|
|
19
|
+
})).toEqual({ allowed: true, toolName: "execShell" });
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
test("requires both env allowlist and agent declaration", () => {
|
package/commandRegistry.ts
CHANGED
|
@@ -69,8 +69,8 @@ export function renderHelpText() {
|
|
|
69
69
|
" nolo space read 01KKY77TT0DA9NY7TNW3R7255N --content-key page-user-id --brief",
|
|
70
70
|
" nolo space delete --name-prefix rn_owner_verify_0504 --yes",
|
|
71
71
|
" nolo table query --table 01ABCXYZ",
|
|
72
|
-
' nolo table query --table meta-
|
|
73
|
-
' nolo table update-row --table meta-
|
|
72
|
+
' nolo table query --table meta-0e95801d90-NOLOTASKBOARD --columns \'["title","status","owner","priority","codeStatus"]\' --no-base-fields --output items',
|
|
73
|
+
' nolo table update-row --table meta-0e95801d90-NOLOTASKBOARD --row 01ROWID --changes \'{"status":"已完成"}\'',
|
|
74
74
|
" nolo task-run read-context --row-dbkey row-user-table-row",
|
|
75
75
|
" nolo llama status",
|
|
76
76
|
];
|
|
@@ -48,6 +48,25 @@ export function createLevelAuthorityStore(
|
|
|
48
48
|
const levelDb: LevelLike = typeof source === "string"
|
|
49
49
|
? new Level<string, any>(source, { valueEncoding: safeJsonEncoding })
|
|
50
50
|
: source;
|
|
51
|
+
const writeBatchOps = async (ops: AuthorityBatchOperation[]) => {
|
|
52
|
+
const result = levelDb.batch(ops);
|
|
53
|
+
if (result && typeof (result as any).then === "function") {
|
|
54
|
+
await result;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (result && typeof (result as any).write === "function") {
|
|
58
|
+
for (const op of ops) {
|
|
59
|
+
if (op.type === "put") {
|
|
60
|
+
(result as LevelBatchWriterLike).put(op.key, op.value);
|
|
61
|
+
} else {
|
|
62
|
+
(result as LevelBatchWriterLike).del(op.key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
await (result as LevelBatchWriterLike).write();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
throw new Error("Level backing does not support batch writes");
|
|
69
|
+
};
|
|
51
70
|
|
|
52
71
|
return {
|
|
53
72
|
get location() {
|
|
@@ -72,7 +91,7 @@ export function createLevelAuthorityStore(
|
|
|
72
91
|
await levelDb.del(key);
|
|
73
92
|
},
|
|
74
93
|
async batchWrite(ops: AuthorityBatchOperation[]) {
|
|
75
|
-
await
|
|
94
|
+
await writeBatchOps(ops);
|
|
76
95
|
},
|
|
77
96
|
createBatch(): AuthorityBatchWriter {
|
|
78
97
|
const batch = levelDb.batch();
|
package/machineWsRunDispatch.ts
CHANGED
|
@@ -20,9 +20,52 @@ type EnvLike = Record<string, string | undefined>;
|
|
|
20
20
|
export type LocalCliExecutor = (
|
|
21
21
|
provider: string,
|
|
22
22
|
prompt: string,
|
|
23
|
-
options: {
|
|
23
|
+
options: {
|
|
24
|
+
model?: string;
|
|
25
|
+
timeout?: number;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
yolo?: boolean;
|
|
28
|
+
env?: EnvLike;
|
|
29
|
+
imageInputs?: Array<{ source: string; materializedPath?: string }>;
|
|
30
|
+
}
|
|
24
31
|
) => Promise<{ text: string; raw?: string; elapsed?: number }>;
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 从 connector payload 的 userInput 中提取 multimodal 内容。
|
|
35
|
+
* 如果 userInput 是字符串,直接返回 { text, imageUrls: [] }。
|
|
36
|
+
* 如果 userInput 是 multimodal content array(`[{ type: "text" }, { type: "image_url" }]`),
|
|
37
|
+
* 提取 text 和 image_url parts。
|
|
38
|
+
*/
|
|
39
|
+
function extractMultimodalUserInput(userInput: unknown): {
|
|
40
|
+
text: string;
|
|
41
|
+
imageUrls: string[];
|
|
42
|
+
} {
|
|
43
|
+
if (typeof userInput === "string") {
|
|
44
|
+
return { text: userInput, imageUrls: [] };
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(userInput)) {
|
|
47
|
+
return { text: String(userInput ?? ""), imageUrls: [] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const textParts: string[] = [];
|
|
51
|
+
const imageUrls: string[] = [];
|
|
52
|
+
|
|
53
|
+
for (const part of userInput) {
|
|
54
|
+
if (!part || typeof part !== "object") continue;
|
|
55
|
+
if ((part as any).type === "text" && typeof (part as any).text === "string") {
|
|
56
|
+
textParts.push((part as any).text);
|
|
57
|
+
} else if (
|
|
58
|
+
(part as any).type === "image_url" &&
|
|
59
|
+
typeof (part as any).image_url?.url === "string" &&
|
|
60
|
+
(part as any).image_url.url.trim()
|
|
61
|
+
) {
|
|
62
|
+
imageUrls.push((part as any).image_url.url.trim());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { text: textParts.join("\n"), imageUrls };
|
|
67
|
+
}
|
|
68
|
+
|
|
26
69
|
type ConnectorRuntimePolicy = {
|
|
27
70
|
runtimeTools?: string[];
|
|
28
71
|
agentTools?: string[];
|
|
@@ -103,7 +146,6 @@ function summarizeRuntimePolicyForLogs(policy?: ConnectorRuntimePolicy) {
|
|
|
103
146
|
workspaceMode: policy?.workspace?.mode ?? null,
|
|
104
147
|
workspaceCwd: policy?.workspace?.cwd ?? null,
|
|
105
148
|
shellEnabled: policy?.shell?.enabled ?? null,
|
|
106
|
-
shellMode: policy?.shell?.mode ?? null,
|
|
107
149
|
};
|
|
108
150
|
}
|
|
109
151
|
|
|
@@ -188,13 +230,13 @@ function runtimeWorkspacePermissionPolicy(
|
|
|
188
230
|
runtimeEnv: EnvLike,
|
|
189
231
|
cwd: string
|
|
190
232
|
): PermissionPolicy | null {
|
|
233
|
+
void runtimeEnv;
|
|
191
234
|
if (!requestsLocalWorkspaceRuntime(runtimePolicy)) return null;
|
|
192
235
|
return {
|
|
193
236
|
mode: "full_access",
|
|
194
237
|
allowFilesystemRead: true,
|
|
195
238
|
allowFilesystemWrite: true,
|
|
196
239
|
allowShell:
|
|
197
|
-
runtimeEnv.NOLO_LOCAL_SHELL_MODE === "worktree" &&
|
|
198
240
|
Array.isArray(runtimePolicy?.runtimeTools) &&
|
|
199
241
|
runtimePolicy.runtimeTools.includes("execShell"),
|
|
200
242
|
writableRoots: [cwd],
|
|
@@ -332,8 +374,6 @@ async function defaultRunConnectorLocalRuntimeAgent(args: {
|
|
|
332
374
|
requestId: args.parsed.requestId,
|
|
333
375
|
agentKey,
|
|
334
376
|
cwd: args.cwd,
|
|
335
|
-
runtimeEnvShellModeBeforePolicy: args.runtimeEnv.NOLO_LOCAL_SHELL_MODE ?? null,
|
|
336
|
-
runtimeEnvShellModeAfterPolicy: env.NOLO_LOCAL_SHELL_MODE ?? null,
|
|
337
377
|
...summarizeRuntimePolicyForLogs(policy),
|
|
338
378
|
});
|
|
339
379
|
const adapter = createCliLocalRuntimeAdapter({
|
|
@@ -454,7 +494,9 @@ export async function handleConnectorRunMessage(
|
|
|
454
494
|
try {
|
|
455
495
|
const machinePermissionPolicy = resolveMachineRunPermissionPolicy(agentConfig);
|
|
456
496
|
const runtimePolicy = runtimePolicyFromConnectorPayload(parsed);
|
|
457
|
-
const
|
|
497
|
+
const extractedInput = extractMultimodalUserInput(parsed.payload?.userInput);
|
|
498
|
+
const userInput = extractedInput.text;
|
|
499
|
+
const connectorImageUrls = extractedInput.imageUrls;
|
|
458
500
|
const timeout = normalizeConnectorRunTimeoutMs(parsed.payload?.timeoutMs);
|
|
459
501
|
let cwd = resolveConnectorRunCwd({ env: runtimeEnv, policy: machinePermissionPolicy });
|
|
460
502
|
let runContent = "";
|
|
@@ -553,6 +595,9 @@ export async function handleConnectorRunMessage(
|
|
|
553
595
|
effectiveRuntimeEnv.AUTH_TOKEN ||
|
|
554
596
|
effectiveRuntimeEnv.AUTH,
|
|
555
597
|
},
|
|
598
|
+
...(connectorImageUrls.length > 0
|
|
599
|
+
? { imageInputs: connectorImageUrls.map((url) => ({ source: url })) }
|
|
600
|
+
: {}),
|
|
556
601
|
}).finally(() => clearInterval(heartbeat));
|
|
557
602
|
logConnectorRun("agent.run.cli.result", {
|
|
558
603
|
requestId: parsed.requestId,
|
|
@@ -40,15 +40,17 @@ function readFlagValue(args: string[], flag: string) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function parseUserIdFromToken(token: string) {
|
|
43
|
-
const payload = token.split(".")[
|
|
44
|
-
if (!payload) return
|
|
43
|
+
const payload = token.split(".")[1];
|
|
44
|
+
if (!payload) return undefined;
|
|
45
45
|
try {
|
|
46
46
|
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
47
47
|
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
48
48
|
const parsed = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
49
|
-
return typeof parsed?.userId === "string"
|
|
49
|
+
return typeof parsed?.userId === "string" && parsed.userId.trim()
|
|
50
|
+
? parsed.userId.trim()
|
|
51
|
+
: undefined;
|
|
50
52
|
} catch {
|
|
51
|
-
return
|
|
53
|
+
return undefined;
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nolo-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-first terminal workspace for Nolo",
|
|
6
6
|
"bin": {
|
|
@@ -169,6 +169,7 @@
|
|
|
169
169
|
"agent-runtime/hybridRecordStore.ts",
|
|
170
170
|
"agent-runtime/localToolPolicy.ts",
|
|
171
171
|
"agent-runtime/shellCommandPolicy.ts",
|
|
172
|
+
"agent-runtime/runtimeToolSurface.ts",
|
|
172
173
|
"connector-experimental/heartbeatLoop.ts",
|
|
173
174
|
"connector-experimental/capabilities.ts",
|
|
174
175
|
"connector-experimental/codexBinary.ts",
|