jeo-code 0.12.13 → 0.12.15
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/CHANGELOG.md +42 -0
- package/README.ja.md +32 -8
- package/README.ko.md +32 -8
- package/README.md +32 -8
- package/README.zh.md +32 -8
- package/package.json +4 -2
- package/src/agent/browser-tool.ts +9 -1
- package/src/agent/config-schema.ts +8 -0
- package/src/agent/context-files.ts +19 -0
- package/src/agent/engine.ts +25 -6
- package/src/agent/eval-tool.ts +6 -0
- package/src/agent/goal-verifier.ts +30 -6
- package/src/agent/hooks.ts +52 -4
- package/src/agent/job-registry.ts +110 -18
- package/src/agent/job-tool.ts +43 -9
- package/src/agent/loop-guards.ts +17 -2
- package/src/agent/memory.ts +151 -112
- package/src/agent/monitor-registry.ts +142 -30
- package/src/agent/monitor-tool.ts +34 -6
- package/src/agent/notify/discord-daemon.ts +135 -42
- package/src/agent/notify/session-endpoint.ts +27 -5
- package/src/agent/sandbox/assets.d.ts +4 -0
- package/src/agent/sandbox/authority.ts +295 -0
- package/src/agent/sandbox/bundle.ts +37 -0
- package/src/agent/sandbox/control.ts +49 -0
- package/src/agent/sandbox/docker.ts +283 -0
- package/src/agent/sandbox/hands-source.text +14548 -0
- package/src/agent/sandbox/hands-worker.ts +168 -0
- package/src/agent/sandbox/policy.ts +155 -0
- package/src/agent/sandbox/rpc.ts +90 -0
- package/src/agent/sandbox/supervisor.ts +210 -0
- package/src/agent/sandbox/types.ts +77 -0
- package/src/agent/shell-lease.ts +190 -0
- package/src/agent/state.ts +167 -0
- package/src/agent/subagents.ts +4 -3
- package/src/agent/task-tool.ts +8 -0
- package/src/agent/todo-tool.ts +30 -1
- package/src/agent/tool-schemas.ts +13 -0
- package/src/ai/model-discovery.ts +12 -0
- package/src/ai/providers/anthropic.ts +27 -2
- package/src/ai/providers/antigravity.ts +10 -2
- package/src/ai/providers/gemini.ts +20 -16
- package/src/ai/providers/ollama.ts +14 -0
- package/src/ai/providers/openai-responses.ts +22 -5
- package/src/ai/providers/openai.ts +23 -40
- package/src/cli/runner.ts +3 -0
- package/src/commands/approve.ts +37 -13
- package/src/commands/launch/flags.ts +2 -1
- package/src/commands/launch/jobs-slash.ts +1 -1
- package/src/commands/launch.ts +261 -35
- package/src/commands/messenger.ts +105 -39
- package/src/commands/notify.ts +2 -2
- package/src/commands/team.ts +44 -12
- package/src/commands/ultragoal.ts +6 -16
- package/src/messenger/activity.ts +2 -1
- package/src/messenger/config.ts +51 -9
- package/src/messenger/discord-setup.ts +122 -20
- package/src/messenger/discord.ts +37 -43
- package/src/messenger/gateway.ts +33 -10
- package/src/messenger/model-selection.ts +101 -0
- package/src/messenger/session-pool.ts +189 -35
- package/src/messenger/types.ts +3 -2
- package/src/skills/catalog.ts +207 -0
|
@@ -60,7 +60,7 @@ export async function runJobsSlash(
|
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const result = await createJobTool(registry)(args, cwd);
|
|
63
|
+
const result = await createJobTool(registry, false)(args, cwd);
|
|
64
64
|
if (result.output) logLines(result.output.split("\n"));
|
|
65
65
|
if (!result.success && result.error) logLines([`! ${result.error}`]);
|
|
66
66
|
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { createInterface } from "node:readline/promises";
|
|
|
2
2
|
import { emitKeypressEvents } from "node:readline";
|
|
3
3
|
import { StringDecoder } from "node:string_decoder";
|
|
4
4
|
import { PassThrough } from "node:stream";
|
|
5
|
-
import {
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { runAgentLoop, DEFAULT_TOOLS, SANDBOX_MEDIATED_TOOLS, TOOL_PROTOCOL, WORKING_DISCIPLINE, KARPATHY_DISCIPLINE, OUTPUT_DISCIPLINE, VERIFICATION_DIRECTIVE, isSafetyFallbackBail, stripSafetyFallbackTag, type AgentLoopEvents } from "../agent/engine";
|
|
6
7
|
import { computerSupervisor } from "../agent/computer-supervisor";
|
|
7
8
|
import { executeComputerAction } from "./computer";
|
|
8
9
|
|
|
@@ -14,8 +15,9 @@ import { createTaskTool, taskToolProtocolLine, type TaskSubEvent } from "../agen
|
|
|
14
15
|
import { createEvalTool, evalToolProtocolLine } from "../agent/eval-tool";
|
|
15
16
|
import { createSubagentTool, SUBAGENT_TOOL_PROTOCOL_LINE } from "../agent/subagent-tool";
|
|
16
17
|
import { SubagentRegistry } from "../agent/subagent-registry";
|
|
17
|
-
import { stopSessionNotifyEndpoint, startSessionNotifyEndpoint
|
|
18
|
+
import { SessionNotifyEndpoint, stopSessionNotifyEndpoint, startSessionNotifyEndpoint } from "../agent/notify/session-endpoint";
|
|
18
19
|
import { parseInThreadConfigCommand } from "../agent/notify/config-commands";
|
|
20
|
+
import { jeoHomeDir, notifySessionsDir } from "../agent/notify/paths";
|
|
19
21
|
|
|
20
22
|
import { createTodoTool, TODO_TOOL_PROTOCOL_LINE } from "../agent/todo-tool";
|
|
21
23
|
import { createJobTool, JOB_TOOL_PROTOCOL_LINE } from "../agent/job-tool";
|
|
@@ -30,7 +32,7 @@ import { runDeepInterviewEngine, type DeepInterviewEngineOptions } from "./deep-
|
|
|
30
32
|
import { runRalplanEngine, type RalplanEngineOptions } from "./ralplan";
|
|
31
33
|
import { runTeamEngine, type TeamEngineOptions } from "./team";
|
|
32
34
|
import { runUltragoalEngine, type UltragoalEngineOptions } from "./ultragoal";
|
|
33
|
-
import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, skillSlashAliases, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
|
|
35
|
+
import { prepareSandboxSkillCatalog, updateSandboxSkillCatalogRoots, skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, skillSlashAliases, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
|
|
34
36
|
import { formatForgeBox, scaleForgeWidth } from "../tui/components/forge";
|
|
35
37
|
import { interactiveOAuthLogin } from "./auth";
|
|
36
38
|
import { logoutOAuth, OAUTH_PROVIDERS, API_KEY_ONLY_PROVIDERS, setApiKey } from "../auth";
|
|
@@ -55,7 +57,12 @@ import { callLlm, type Message } from "../agent/loop";
|
|
|
55
57
|
import { friendlyProviderError } from "../util/provider-error";
|
|
56
58
|
import { readGlobalConfig, saveConfigPatch, resolveWikiRoot } from "../agent/state";
|
|
57
59
|
import { AgyCliError, type AgyTurnResult } from "../agent/antigravity-cli";
|
|
58
|
-
import { runPreToolHooks, runPostTurnHooks } from "../agent/hooks";
|
|
60
|
+
import { currentSandboxExecution, withSandboxExecution, runPreToolHooks, runPostTurnHooks } from "../agent/hooks";
|
|
61
|
+
import { createSandboxSupervisor } from "../agent/sandbox/supervisor";
|
|
62
|
+
import { createSandboxPolicy } from "../agent/sandbox/policy";
|
|
63
|
+
import { createGrantAuthority } from "../agent/sandbox/authority";
|
|
64
|
+
import { handleSandboxControl } from "../agent/sandbox/control";
|
|
65
|
+
import type { SandboxConfig, SandboxSupervisor } from "../agent/sandbox/types";
|
|
59
66
|
import { runAgySessionTurn } from "./launch/agy-turn";
|
|
60
67
|
import { listProviderModels } from "../ai/model-discovery";
|
|
61
68
|
import { rememberModelPatch, recentModelsForDisplay } from "../agent/model-recency";
|
|
@@ -387,6 +394,102 @@ export function formatResumeHint(sessionId: string): string {
|
|
|
387
394
|
}
|
|
388
395
|
|
|
389
396
|
export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
397
|
+
// Brain-only control secret: remove before any tool, catalog, or guest setup.
|
|
398
|
+
const shutdownNonce = process.env.JEO_MESSENGER_SHUTDOWN_NONCE;
|
|
399
|
+
delete process.env.JEO_MESSENGER_SHUTDOWN_NONCE;
|
|
400
|
+
let supervisor: SandboxSupervisor | undefined;
|
|
401
|
+
let stateDirectory: string | undefined;
|
|
402
|
+
let stopEndpoint: (() => Promise<void>) | undefined;
|
|
403
|
+
let endpointStartup: Promise<void> | undefined;
|
|
404
|
+
let closing: Promise<void> | undefined;
|
|
405
|
+
const acknowledge = async (gone: boolean): Promise<void> => {
|
|
406
|
+
if (!shutdownNonce) return;
|
|
407
|
+
const written = Promise.withResolvers<void>();
|
|
408
|
+
process.stderr.write(`JEO_MESSENGER_SHUTDOWN_ACK ${JSON.stringify({ nonce: shutdownNonce, gone })}\n`, error => error ? written.reject(error) : written.resolve());
|
|
409
|
+
await written.promise;
|
|
410
|
+
};
|
|
411
|
+
const close = (): Promise<void> => closing ??= (async () => {
|
|
412
|
+
for (;;) {
|
|
413
|
+
try {
|
|
414
|
+
await endpointStartup;
|
|
415
|
+
await stopEndpoint?.();
|
|
416
|
+
// No supervisor means startup never had authority to create an environment.
|
|
417
|
+
await supervisor?.close();
|
|
418
|
+
if (stateDirectory) fs.rmSync(stateDirectory, { recursive: true, force: true });
|
|
419
|
+
await acknowledge(true);
|
|
420
|
+
return;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error(`REVOKE_INCOMPLETE: ${error instanceof Error ? error.message : String(error)}`);
|
|
423
|
+
await acknowledge(false).catch(() => { /* A broken parent pipe must not destroy the cleanup owner. */ });
|
|
424
|
+
// Keep the sole cleanup owner and its private state alive until removal succeeds.
|
|
425
|
+
const retry = Promise.withResolvers<void>();
|
|
426
|
+
setTimeout(retry.resolve, 1_000);
|
|
427
|
+
await retry.promise;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
})();
|
|
431
|
+
const handleTermination = (): void => { void close().then(() => process.exit(143)); };
|
|
432
|
+
process.on("SIGTERM", handleTermination);
|
|
433
|
+
// Private cross-platform control: the pool's IPC channel (not a signal, not stdin) carries the shutdown
|
|
434
|
+
// request. Installed before any await so no setup phase can miss it; accepts only the exact nonce message.
|
|
435
|
+
const handleControlMessage = (message: unknown): void => {
|
|
436
|
+
if (!shutdownNonce || typeof message !== "object" || message === null || Array.isArray(message)) return;
|
|
437
|
+
const keys = Object.keys(message);
|
|
438
|
+
if (keys.length !== 2 || !keys.includes("type") || !keys.includes("nonce")) return;
|
|
439
|
+
const control = message as { type?: unknown; nonce?: unknown };
|
|
440
|
+
if (control.type !== "jeo_messenger_shutdown" || control.nonce !== shutdownNonce) return;
|
|
441
|
+
handleTermination();
|
|
442
|
+
};
|
|
443
|
+
if (shutdownNonce) process.on("message", handleControlMessage);
|
|
444
|
+
const previousSessionDirectory = process.env.JEO_SESSION_DIR;
|
|
445
|
+
const runSession = (): Promise<void> => {
|
|
446
|
+
const ready = Promise.withResolvers<void>();
|
|
447
|
+
endpointStartup = ready.promise;
|
|
448
|
+
return runLaunchSession(args, cleanup => { stopEndpoint = cleanup; ready.resolve(); }).finally(ready.resolve);
|
|
449
|
+
};
|
|
450
|
+
try {
|
|
451
|
+
const flags = parseFlags(args, process.cwd());
|
|
452
|
+
const config = await readGlobalConfig();
|
|
453
|
+
if (closing) throw new Error("Messenger worker stopping");
|
|
454
|
+
const remoteConfig = process.env.JEO_MESSENGER_SANDBOX;
|
|
455
|
+
const sandboxConfig: SandboxConfig | undefined = remoteConfig === undefined ? config.sandbox : JSON.parse(remoteConfig);
|
|
456
|
+
if ((flags.literalPrompt || process.env.JEO_MESSENGER_SESSION_KEY) && !sandboxConfig) throw new Error("SANDBOX_REQUIRED: remote execution requires sandbox configuration");
|
|
457
|
+
if (!sandboxConfig) return await runSession();
|
|
458
|
+
const cwd = flags.worktree ? resolveWorktree(process.cwd(), flags.worktree) : process.cwd();
|
|
459
|
+
const sessionKey = process.env.JEO_MESSENGER_SESSION_KEY || crypto.randomUUID();
|
|
460
|
+
for (const legacy of [path.join(cwd, ".jeo", "sessions"), path.join(cwd, ".jeo", "messenger-sessions"), path.join(cwd, ".jeo", "memory", "raw")]) {
|
|
461
|
+
if (fs.existsSync(legacy)) throw new Error("SANDBOX_PRIVATE_STATE: legacy transcripts are inside the workspace; relocate them before granting workspace access");
|
|
462
|
+
}
|
|
463
|
+
stateDirectory = fs.mkdtempSync(path.join(tmpdir(), "jeo-sandbox-authority-"));
|
|
464
|
+
const sessionDirectory = previousSessionDirectory || path.join(jeoHomeDir(), "sandbox-sessions", createHash("sha256").update(path.resolve(cwd)).digest("hex"));
|
|
465
|
+
const privateRoots = [stateDirectory, sessionDirectory, jeoHomeDir(), notifySessionsDir()];
|
|
466
|
+
const policy = await createSandboxPolicy({ sessionKey, revision: 0, cwd, config: sandboxConfig, privatePaths: privateRoots });
|
|
467
|
+
if (closing) throw new Error("Messenger worker stopping");
|
|
468
|
+
const sessionSupervisor = createSandboxSupervisor({ policy });
|
|
469
|
+
supervisor = sessionSupervisor;
|
|
470
|
+
process.env.JEO_SESSION_DIR = sessionDirectory;
|
|
471
|
+
await prepareSandboxSkillCatalog(sessionSupervisor, cwd, policy.mounts.filter(mount => mount.mode === "rw").map(mount => mount.host), policy.mounts.map(mount => mount.host));
|
|
472
|
+
if (closing) throw new Error("Messenger worker stopping");
|
|
473
|
+
const replacePolicy = sessionSupervisor.replacePolicy;
|
|
474
|
+
sessionSupervisor.replacePolicy = async nextPolicy => {
|
|
475
|
+
updateSandboxSkillCatalogRoots(sessionSupervisor, nextPolicy.mounts.filter(mount => mount.mode === "rw").map(mount => mount.host), nextPolicy.mounts.map(mount => mount.host));
|
|
476
|
+
await replacePolicy(nextPolicy);
|
|
477
|
+
};
|
|
478
|
+
const allowedUserIds: string[] = remoteConfig === undefined ? config.notifications?.discord?.allowedUserIds ?? [] : JSON.parse(process.env.JEO_MESSENGER_ALLOWED_USER_IDS || "[]");
|
|
479
|
+
if (!Array.isArray(allowedUserIds) || allowedUserIds.some(id => typeof id !== "string" || !id.trim())) throw new Error("UNAUTHORIZED: invalid sandbox actor allowlist");
|
|
480
|
+
const authority = await createGrantAuthority({ sessionKey, cwd, config: sandboxConfig, supervisor: sessionSupervisor, allowedUserIds, stateDirectory, privateRoots });
|
|
481
|
+
if (closing) throw new Error("Messenger worker stopping");
|
|
482
|
+
await withSandboxExecution({ supervisor: sessionSupervisor, authority, requestAccess: (host, mode) => authority.requestAccess(host, mode) }, runSession);
|
|
483
|
+
} finally {
|
|
484
|
+
await close();
|
|
485
|
+
process.removeListener("SIGTERM", handleTermination);
|
|
486
|
+
process.removeListener("message", handleControlMessage);
|
|
487
|
+
if (previousSessionDirectory === undefined) delete process.env.JEO_SESSION_DIR;
|
|
488
|
+
else process.env.JEO_SESSION_DIR = previousSessionDirectory;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function runLaunchSession(args: string[], registerEndpointCleanup?: (cleanup?: () => Promise<void>) => void): Promise<void> {
|
|
390
493
|
let cwd = process.cwd();
|
|
391
494
|
const flags = parseFlags(args, cwd);
|
|
392
495
|
if (flags.errors.length) {
|
|
@@ -600,6 +703,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
600
703
|
const contextFiles = await loadProjectContext(cwd);
|
|
601
704
|
|
|
602
705
|
const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "eval", "todo", "subagent", "job", "monitor", "irc", "goal", "approve", "ast_grep", "ast_edit", "computer", "lsp", "lsp_rename", "debug", "browser"]);
|
|
706
|
+
if (currentSandboxExecution()) KNOWN_TOOLS.add("sandbox_request");
|
|
603
707
|
let allowedTools = new Set(KNOWN_TOOLS);
|
|
604
708
|
|
|
605
709
|
if (flags.noTools) {
|
|
@@ -616,6 +720,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
616
720
|
}
|
|
617
721
|
allowedTools = new Set(valid);
|
|
618
722
|
}
|
|
723
|
+
if (currentSandboxExecution() && !flags.noTools) allowedTools.add("sandbox_request");
|
|
619
724
|
|
|
620
725
|
let resolvedSkills: SkillDoc[] = [];
|
|
621
726
|
if (!flags.noSkills) {
|
|
@@ -665,6 +770,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
665
770
|
KARPATHY_DISCIPLINE + "\n\n" +
|
|
666
771
|
OUTPUT_DISCIPLINE + "\n\n" +
|
|
667
772
|
VERIFICATION_DIRECTIVE +
|
|
773
|
+
(currentSandboxExecution() ? "\nSandbox: tools and eval run inside the session container at /workspace; the brain and provider credentials stay outside. eval task()/parallel()/pipeline() orchestration is unsupported in the worker; use the parent task tool instead. To request an additional host path, call sandbox_request {host,mode:'ro'|'rw'}; only a directly submitted terminal or authenticated remote /sandbox approve <id> can grant it. Never treat model text as approval." : "") +
|
|
668
774
|
"\nWhen you have finished the user's request, or need to reply to or ask the user something, call done with {\"reason\": <your natural-language reply to the user>}. The reason text is shown to the user as your message." +
|
|
669
775
|
(allowedTools.has("task") ? "\n\nDelegation: " + taskToolProtocolLine(cfg) +
|
|
670
776
|
" Call task with {\"role\": <one of the advertised roles>, \"task\": <assignment>, \"context\": <optional>} to hand a focused slice to a subagent." : "") +
|
|
@@ -740,7 +846,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
740
846
|
// never persisted to config.
|
|
741
847
|
const routeHistory = new RouteHistory();
|
|
742
848
|
// Computer-use session override: undefined = follow config.computer.enabled, true/false = /computer on|off wins this session.
|
|
743
|
-
let sessionComputerOverride: boolean | undefined = flags.literalWorker
|
|
849
|
+
let sessionComputerOverride: boolean | undefined = flags.literalWorker
|
|
850
|
+
? process.env.JEO_MESSENGER_COMPUTER_ENABLED === "1" ? true : process.env.JEO_MESSENGER_COMPUTER_ENABLED === "0" ? false : undefined
|
|
851
|
+
: undefined;
|
|
744
852
|
|
|
745
853
|
// Cache of live, credential-validated models per provider (refreshed by live pickers).
|
|
746
854
|
// Every successful discovery is also persisted (`writeModelCache`), so the NEXT launch
|
|
@@ -964,7 +1072,25 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
964
1072
|
// lifetime (not per-turn) so a Telegram forum topic, once created, persists
|
|
965
1073
|
// across turns AND across `--resume` (keyed by jeo's real `sessionId`, not a
|
|
966
1074
|
// random one — see `SessionNotifyEndpoint`'s header doc).
|
|
967
|
-
const
|
|
1075
|
+
const sandboxAuthority = currentSandboxExecution()?.authority;
|
|
1076
|
+
const sessionNotifyEndpoint = flags.literalPrompt
|
|
1077
|
+
? (sandboxAuthority ? new SessionNotifyEndpoint(cwd, process.env.JEO_MESSENGER_SESSION_KEY || sessionId) : undefined)
|
|
1078
|
+
: await startSessionNotifyEndpoint(cwd, sessionId);
|
|
1079
|
+
try {
|
|
1080
|
+
if (sessionNotifyEndpoint && sandboxAuthority) {
|
|
1081
|
+
sessionNotifyEndpoint.onSandboxControl = (text, actor) => {
|
|
1082
|
+
if (actor.sessionKey !== sessionNotifyEndpoint.sessionId) return Promise.resolve({ handled: true, output: "SESSION_MISMATCH" });
|
|
1083
|
+
return handleSandboxControl(text, { ...actor, sessionKey: sandboxAuthority.snapshot().sessionKey }, sandboxAuthority);
|
|
1084
|
+
};
|
|
1085
|
+
if (flags.literalPrompt) {
|
|
1086
|
+
sessionNotifyEndpoint.setDiscordControlEnabled(true);
|
|
1087
|
+
await sessionNotifyEndpoint.start();
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
} finally {
|
|
1091
|
+
registerEndpointCleanup?.(sessionNotifyEndpoint ? () => sessionNotifyEndpoint.stop() : undefined);
|
|
1092
|
+
}
|
|
1093
|
+
const sandboxTranscriptId = sessionId;
|
|
968
1094
|
if (sessionNotifyEndpoint) {
|
|
969
1095
|
sessionNotifyEndpoint.sendIdentity({ repo: cwd.split(/[\\/]/).filter(Boolean).pop() ?? cwd, branch, cwd });
|
|
970
1096
|
}
|
|
@@ -1007,6 +1133,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1007
1133
|
sessionNotifyEndpoint?.detachRegistry();
|
|
1008
1134
|
await sessionNotifyEndpoint?.stop();
|
|
1009
1135
|
await stopSessionNotifyEndpoint(subagentRegistry);
|
|
1136
|
+
await turnSandboxScope?.supervisor.close();
|
|
1010
1137
|
};
|
|
1011
1138
|
|
|
1012
1139
|
// Persist the active per-session model into the session header so `/resume` restores
|
|
@@ -1077,7 +1204,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1077
1204
|
|
|
1078
1205
|
// Run one conversational turn: compact, persist user msg, run the loop, persist + return the reply.
|
|
1079
1206
|
// When `useTui`, a live TUI renders the turn and prints the final reply itself (rendered=true).
|
|
1080
|
-
const
|
|
1207
|
+
const turnSandboxScope = currentSandboxExecution();
|
|
1208
|
+
const runTurnScoped = async (
|
|
1081
1209
|
userInput: string,
|
|
1082
1210
|
useTui: boolean,
|
|
1083
1211
|
images?: ImageAttachment[],
|
|
@@ -1088,6 +1216,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1088
1216
|
opts?: { userCard?: string | null; signal?: AbortSignal; trustedContext?: string },
|
|
1089
1217
|
): Promise<{ done: boolean; steps: number; reply: string; rendered: boolean; usage: string }> => {
|
|
1090
1218
|
opts?.signal?.throwIfAborted();
|
|
1219
|
+
if (currentSandboxExecution() && sessionId !== sandboxTranscriptId) throw new Error("SESSION_MISMATCH: start a new launch for the selected transcript; sandbox grants cannot cross sessions");
|
|
1091
1220
|
let busyDraftHasPaste = false;
|
|
1092
1221
|
const captureLiveInput = (chunk: string, view: LaunchTui | null): { captured: boolean; submittedEnter: boolean; handled: boolean } => {
|
|
1093
1222
|
if (!queueBusyInput || !queueBusySnapshot) return { captured: false, submittedEnter: false, handled: false };
|
|
@@ -1097,6 +1226,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1097
1226
|
const captured = queueBusyInput?.(chunk) ?? false;
|
|
1098
1227
|
const draft = expandSentinel(queueBusySnapshot?.().text ?? "");
|
|
1099
1228
|
if (!draft && !paste && !(queueBusyPasteActive?.() ?? false)) busyDraftHasPaste = false;
|
|
1229
|
+
if (submittedEnter && /^\/sandbox(?:\s|$)/.test(draft.trim())) {
|
|
1230
|
+
const report = (message: string): void => { if (view) view.events().onNotice?.(message); else console.log(message); };
|
|
1231
|
+
if (busyDraftHasPaste || !process.stdin.isTTY || !sandboxAuthority) {
|
|
1232
|
+
report("UNAUTHORIZED: sandbox controls require an active sandbox and directly submitted local prompt.");
|
|
1233
|
+
} else {
|
|
1234
|
+
queueBusyClear?.();
|
|
1235
|
+
view?.setLivePromptInput("");
|
|
1236
|
+
void handleSandboxControl(draft.trim(), { origin: "terminal", sessionKey: sandboxAuthority.snapshot().sessionKey }, sandboxAuthority)
|
|
1237
|
+
.then(result => report(result.output), error => report(error instanceof Error ? error.message : String(error)));
|
|
1238
|
+
}
|
|
1239
|
+
return { captured, submittedEnter, handled: true };
|
|
1240
|
+
}
|
|
1100
1241
|
if (submittedEnter && /^\/discord\s+control\s+off$/.test(draft.trim())) {
|
|
1101
1242
|
const endpoint = sessionNotifyEndpoint;
|
|
1102
1243
|
if (!busyDraftHasPaste && endpoint && endpoint.sessionId === sessionId) {
|
|
@@ -1139,6 +1280,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1139
1280
|
};
|
|
1140
1281
|
const selectedModel = sessionModel || defaultModel;
|
|
1141
1282
|
if (selectedModel.startsWith("agy/")) {
|
|
1283
|
+
if (currentSandboxExecution()) throw new Error("SANDBOX_UNSUPPORTED: official CLI agents cannot execute outside the session sandbox; select a raw-model provider");
|
|
1142
1284
|
const oneShot = !!flags.print || flags.message.length > 0 || !process.stdin.isTTY;
|
|
1143
1285
|
if (flags.noTools || flags.tools || flags.maxSteps > 0 || flags.systemPrompt || flags.appendSystemPrompt || flags.thinking) {
|
|
1144
1286
|
if (oneShot) process.exitCode = 1;
|
|
@@ -1755,20 +1897,30 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1755
1897
|
reason: string,
|
|
1756
1898
|
evidence: { sawMutation: boolean; sawVerification: boolean; verificationStale: boolean },
|
|
1757
1899
|
): Promise<string | null> => {
|
|
1758
|
-
const
|
|
1900
|
+
const liveJobs = jobRegistry.running();
|
|
1901
|
+
if (liveJobs.length > 0) {
|
|
1902
|
+
const jobs = liveJobs.map(record => `${record.id} (${record.command})`).join(", ");
|
|
1903
|
+
return `Cannot finish while background job(s) are still running: ${jobs}. Wait for them to exit or cancel them, then run fresh verification before calling done again.`;
|
|
1904
|
+
}
|
|
1905
|
+
const runningJobs = jobRegistry.unobserved();
|
|
1759
1906
|
if (runningJobs.length > 0) {
|
|
1760
1907
|
const jobs = runningJobs.map(record => `${record.id} (${record.command})`).join(", ");
|
|
1761
1908
|
return (
|
|
1762
|
-
`Cannot finish while background job(s)
|
|
1763
|
-
`
|
|
1909
|
+
`Cannot finish while background job(s) have unobserved completions: ${jobs}. ` +
|
|
1910
|
+
`Await them, then run fresh verification before calling done again.`
|
|
1764
1911
|
);
|
|
1765
1912
|
}
|
|
1766
|
-
const
|
|
1913
|
+
const liveMonitors = monitorRegistry.running();
|
|
1914
|
+
if (liveMonitors.length > 0) {
|
|
1915
|
+
const monitors = liveMonitors.map(record => `${record.id} (${record.command})`).join(", ");
|
|
1916
|
+
return `Cannot finish while background monitor(s) are still running: ${monitors}. Wait for them to exit or cancel them, then run fresh verification before calling done again.`;
|
|
1917
|
+
}
|
|
1918
|
+
const runningMonitors = monitorRegistry.unobserved();
|
|
1767
1919
|
if (runningMonitors.length > 0) {
|
|
1768
1920
|
const monitors = runningMonitors.map(record => `${record.id} (${record.command})`).join(", ");
|
|
1769
1921
|
return (
|
|
1770
|
-
`Cannot finish while background monitor(s)
|
|
1771
|
-
`
|
|
1922
|
+
`Cannot finish while background monitor(s) have unobserved completions: ${monitors}. ` +
|
|
1923
|
+
`Await them, then run fresh verification before calling done again.`
|
|
1772
1924
|
);
|
|
1773
1925
|
}
|
|
1774
1926
|
|
|
@@ -1867,14 +2019,41 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1867
2019
|
irc: createIrcTool(subagentRegistry),
|
|
1868
2020
|
goal: createGoalTool(),
|
|
1869
2021
|
approve: createApproveTool(),
|
|
2022
|
+
sandbox_request: async (a: Record<string, any>) => {
|
|
2023
|
+
const scope = currentSandboxExecution();
|
|
2024
|
+
if (!scope?.requestAccess) return { success: false, output: "", error: "No session sandbox authority" };
|
|
2025
|
+
if (a.action === "status" && scope.authority) {
|
|
2026
|
+
const { revision, grants, requests, blocked } = scope.authority.snapshot();
|
|
2027
|
+
return { success: true, output: JSON.stringify({ revision, grants, requests, blocked }) };
|
|
2028
|
+
}
|
|
2029
|
+
if (typeof a.host !== "string" || (a.mode !== "ro" && a.mode !== "rw")) return { success: false, output: "", error: "sandbox_request requires host and mode ro|rw" };
|
|
2030
|
+
const request = await scope.requestAccess(a.host, a.mode);
|
|
2031
|
+
return { success: true, output: JSON.stringify({ request, grants: scope.authority?.snapshot().grants ?? [] }) };
|
|
2032
|
+
},
|
|
1870
2033
|
// `/computer on|off` session override wins over `config.computer.enabled`
|
|
1871
2034
|
// (see `sessionComputerOverride`/`computer-slash.ts`); undefined = fall
|
|
1872
2035
|
// through to DEFAULT_TOOLS' own config-only gate inside executeComputerAction.
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2036
|
+
// Trusted mediated wrapper (SANDBOX_MEDIATED_TOOLS): inside a sandbox the parent
|
|
2037
|
+
// resolves consent from session/config ONLY and forwards it over the private
|
|
2038
|
+
// `__computer` hands handler; the host never executes the action.
|
|
2039
|
+
computer: (() => {
|
|
2040
|
+
const handler = async (a: Record<string, any>, hcwd: string, onProgress?: (text: string) => void) => {
|
|
2041
|
+
const sandbox = currentSandboxExecution();
|
|
2042
|
+
if (sandbox) {
|
|
2043
|
+
const cfg = await readGlobalConfig().catch(() => null);
|
|
2044
|
+
return sandbox.supervisor.call(
|
|
2045
|
+
"__computer",
|
|
2046
|
+
{ action: a, enabled: sessionComputerOverride ?? !!cfg?.computer?.enabled },
|
|
2047
|
+
{ cwd: hcwd, signal: sandbox.signal, onProgress },
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
computerSupervisor.setKillSwitchLive(true);
|
|
2051
|
+
computerSupervisor.heartbeat();
|
|
2052
|
+
return executeComputerAction(a as any, { enabledOverride: sessionComputerOverride });
|
|
2053
|
+
};
|
|
2054
|
+
SANDBOX_MEDIATED_TOOLS.add(handler as any);
|
|
2055
|
+
return handler;
|
|
2056
|
+
})(),
|
|
1878
2057
|
|
|
1879
2058
|
};
|
|
1880
2059
|
const tools = filterToolMap(fullTools, Array.from(allowedTools));
|
|
@@ -2245,15 +2424,21 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2245
2424
|
const usage = result.usage ? ` (${result.usage.inputTokens} in / ${result.usage.outputTokens} out tokens)` : "";
|
|
2246
2425
|
|
|
2247
2426
|
if (!flags.literalPrompt && turnConfig.gitAutoCommit) {
|
|
2427
|
+
const sandbox = currentSandboxExecution();
|
|
2248
2428
|
try {
|
|
2249
|
-
const statusRes =
|
|
2250
|
-
|
|
2251
|
-
Bun.spawnSync(["git", "
|
|
2252
|
-
|
|
2253
|
-
const
|
|
2429
|
+
const statusRes = sandbox
|
|
2430
|
+
? await sandbox.supervisor.exec(["git", "status", "--porcelain"], { cwd })
|
|
2431
|
+
: Bun.spawnSync(["git", "status", "--porcelain"], { cwd, stdout: "pipe", stderr: "ignore" });
|
|
2432
|
+
if (statusRes.exitCode === 0 && statusRes.stdout?.toString().trim()) {
|
|
2433
|
+
const addRes = sandbox ? await sandbox.supervisor.exec(["git", "add", "."], { cwd }) : Bun.spawnSync(["git", "add", "."], { cwd });
|
|
2434
|
+
if (addRes.exitCode !== 0) throw new Error("git add failed");
|
|
2435
|
+
const diffRes = sandbox
|
|
2436
|
+
? await sandbox.supervisor.exec(["git", "diff", "--cached", "--name-status"], { cwd })
|
|
2437
|
+
: Bun.spawnSync(["git", "diff", "--cached", "--name-status"], { cwd, stdout: "pipe", stderr: "ignore" });
|
|
2438
|
+
const changedFiles = (diffRes.stdout?.toString() ?? "").trim().split("\n").map(line => line.split("\t")[1]).filter(Boolean);
|
|
2254
2439
|
const fileSummary = changedFiles.length > 0 ? ` (modified: ${changedFiles.slice(0, 3).join(", ")}${changedFiles.length > 3 ? ", ..." : ""})` : "";
|
|
2255
2440
|
const commitMsg = `[jeo] auto-commit${fileSummary}\n\nPrompt: ${userInput.slice(0, 100)}${userInput.length > 100 ? "..." : ""}`;
|
|
2256
|
-
const commitRes = Bun.spawnSync(["git", "commit", "-m", commitMsg], { cwd });
|
|
2441
|
+
const commitRes = sandbox ? await sandbox.supervisor.exec(["git", "commit", "-m", commitMsg], { cwd }) : Bun.spawnSync(["git", "commit", "-m", commitMsg], { cwd });
|
|
2257
2442
|
if (commitRes.exitCode === 0) {
|
|
2258
2443
|
if (tui) tui.events().onNotice?.(`(auto-committed changes to git)`);
|
|
2259
2444
|
else console.log(`(auto-committed changes to git)`);
|
|
@@ -2278,6 +2463,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2278
2463
|
};
|
|
2279
2464
|
|
|
2280
2465
|
|
|
2466
|
+
const runTurn = (...args: Parameters<typeof runTurnScoped>) => turnSandboxScope
|
|
2467
|
+
? withSandboxExecution(turnSandboxScope, () => runTurnScoped(...args))
|
|
2468
|
+
: runTurnScoped(...args);
|
|
2469
|
+
|
|
2281
2470
|
const joinedArgs = flags.message;
|
|
2282
2471
|
const isOneShot = flags.print || joinedArgs.length > 0 || !process.stdin.isTTY;
|
|
2283
2472
|
// hermes-style experience distill (plan/gjc-inheritance.md B6) for one-shot
|
|
@@ -3064,6 +3253,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3064
3253
|
let promptServedFromPaste = false;
|
|
3065
3254
|
let promptWasRemote = false;
|
|
3066
3255
|
let exclusiveRemote: { text: string; finish: (text?: string, artifactsAllowed?: boolean) => void; signal: AbortSignal; epoch: number; stage?: { output: string; input?: string } } | undefined;
|
|
3256
|
+
let pendingRemote: NonNullable<typeof exclusiveRemote> | undefined;
|
|
3257
|
+
let admitPendingRemote: () => void = () => {};
|
|
3067
3258
|
const promptInput = async (prompt: string): Promise<string> => {
|
|
3068
3259
|
promptServedFromPaste = false;
|
|
3069
3260
|
promptWasRemote = false;
|
|
@@ -3114,6 +3305,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3114
3305
|
}),
|
|
3115
3306
|
new Promise<{ remote: string }>(resolve => {
|
|
3116
3307
|
remoteInjectResolve = resolve;
|
|
3308
|
+
admitPendingRemote();
|
|
3117
3309
|
}),
|
|
3118
3310
|
]);
|
|
3119
3311
|
// The aborted `rl.question()` lost the race (a remote message or stdin-close
|
|
@@ -3650,6 +3842,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3650
3842
|
sessionNotifyEndpoint?.detachRegistry();
|
|
3651
3843
|
void sessionNotifyEndpoint?.stop();
|
|
3652
3844
|
void stopSessionNotifyEndpoint(subagentRegistry);
|
|
3845
|
+
if (turnSandboxScope) {
|
|
3846
|
+
void turnSandboxScope.supervisor.close().then(() => process.exit(130), error => {
|
|
3847
|
+
console.error(`REVOKE_INCOMPLETE: ${error instanceof Error ? error.message : String(error)}`);
|
|
3848
|
+
process.exit(1);
|
|
3849
|
+
});
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3653
3852
|
process.exit(130);
|
|
3654
3853
|
};
|
|
3655
3854
|
// Prompt Ctrl+C: a single press clears a non-empty box; on an empty box it exits.
|
|
@@ -4571,19 +4770,32 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4571
4770
|
// established by now. See the Tier 2 contract's "Session-side routing".
|
|
4572
4771
|
if (sessionNotifyEndpoint) {
|
|
4573
4772
|
sessionNotifyEndpoint.onRegistryMutation = () => { relayInputEpoch++; };
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
const request = { text, finish, signal, stage, epoch: ++relayInputEpoch };
|
|
4773
|
+
admitPendingRemote = (): void => {
|
|
4774
|
+
if (!pendingRemote || exclusiveRemote || interactiveTurnActive || remoteInputPending || promptPasteActive || pasteLineFired || pasteInFlight || draftHasPaste || Reflect.get(rl, "line") !== "" || !pendingRemoteInject ||
|
|
4775
|
+
pendingImages.length || pendingStdinLines.length || pendingMidTurnCommands.length || queuedPromptInput.partial || queuedPromptInput.pastedLines.length || sessionNotifyEndpoint.sessionId !== sessionId) return;
|
|
4776
|
+
const request = pendingRemote;
|
|
4777
|
+
if (request.signal.aborted) { pendingRemote = undefined; request.finish(); return; }
|
|
4778
|
+
request.epoch = ++relayInputEpoch;
|
|
4581
4779
|
exclusiveRemote = request;
|
|
4582
|
-
if (pendingRemoteInject(text))
|
|
4583
|
-
exclusiveRemote = undefined;
|
|
4584
|
-
|
|
4780
|
+
if (pendingRemoteInject(request.text)) pendingRemote = undefined;
|
|
4781
|
+
else exclusiveRemote = undefined;
|
|
4782
|
+
};
|
|
4783
|
+
sessionNotifyEndpoint.onRemoteRequest = (text, finish, signal, stage, actor): boolean => {
|
|
4784
|
+
if (!sandboxAuthority || actor?.origin !== "discord" || actor.sessionKey !== sessionNotifyEndpoint.sessionId ||
|
|
4785
|
+
!cfg.notifications?.discord?.allowedUserIds?.includes(actor.userId ?? "") || pendingRemote || signal.aborted) return false;
|
|
4786
|
+
if (stage && ((sessionModel || defaultModel).startsWith("agy/") || history[0]?.role !== "system")) return false;
|
|
4787
|
+
const request = { text, finish, signal, stage, epoch: 0 };
|
|
4788
|
+
pendingRemote = request;
|
|
4789
|
+
signal.addEventListener("abort", () => {
|
|
4790
|
+
if (pendingRemote === request) { pendingRemote = undefined; finish(); }
|
|
4791
|
+
}, { once: true });
|
|
4792
|
+
admitPendingRemote();
|
|
4793
|
+
return true;
|
|
4585
4794
|
};
|
|
4586
4795
|
sessionNotifyEndpoint.onUserMessage = async (msg): Promise<void | boolean> => {
|
|
4796
|
+
if (!sandboxAuthority) return false;
|
|
4797
|
+
if (msg.actor && (msg.actor.origin !== "discord" || msg.actor.sessionKey !== sessionNotifyEndpoint.sessionId ||
|
|
4798
|
+
!cfg.notifications?.discord?.allowedUserIds?.includes(msg.actor.userId ?? ""))) return false;
|
|
4587
4799
|
relayInputEpoch++;
|
|
4588
4800
|
remoteInputPending++;
|
|
4589
4801
|
try {
|
|
@@ -4768,6 +4980,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4768
4980
|
pendingSelection = undefined;
|
|
4769
4981
|
navMatches = [];
|
|
4770
4982
|
navIdx = -1;
|
|
4983
|
+
if (input === "/sandbox" || input.startsWith("/sandbox ")) {
|
|
4984
|
+
if (!directLocalPrompt || explicitPastedDraft || promptServedFromPaste || remoteInputPending !== 0 ||
|
|
4985
|
+
relayInputEpoch - relayEpoch !== ambiguousPasteEpoch - ambiguousEpoch) {
|
|
4986
|
+
console.log("UNAUTHORIZED: sandbox controls require a directly submitted local prompt.");
|
|
4987
|
+
} else if (!sandboxAuthority) {
|
|
4988
|
+
console.log("No session sandbox is active.");
|
|
4989
|
+
} else {
|
|
4990
|
+
const result = await handleSandboxControl(input, { origin: "terminal", sessionKey: sandboxAuthority.snapshot().sessionKey }, sandboxAuthority);
|
|
4991
|
+
console.log(result.output);
|
|
4992
|
+
}
|
|
4993
|
+
continue;
|
|
4994
|
+
}
|
|
4771
4995
|
if (input === "/discord" || input.startsWith("/discord ")) {
|
|
4772
4996
|
const parts = input.split(/\s+/);
|
|
4773
4997
|
const isControlOn = parts.length === 4 && parts[1] === "control" && parts[2] === "on" && parts[3] === "--allow-full-control";
|
|
@@ -4776,6 +5000,8 @@ navMatches = [];
|
|
|
4776
5000
|
if (!directLocalPrompt || explicitPastedDraft || promptServedFromPaste || remoteInputPending !== 0 ||
|
|
4777
5001
|
relayInputEpoch - relayEpoch !== ambiguousPasteEpoch - ambiguousEpoch) {
|
|
4778
5002
|
console.log("Discord control requires a directly submitted local prompt.");
|
|
5003
|
+
} else if (isControlOn && !sandboxAuthority) {
|
|
5004
|
+
console.log("SANDBOX_REQUIRED: configure a sandbox and start a new launch before enabling Discord control.");
|
|
4779
5005
|
} else if (!sessionNotifyEndpoint || sessionNotifyEndpoint.sessionId !== sessionId) {
|
|
4780
5006
|
console.log("Discord control unavailable: notification session endpoint unavailable.");
|
|
4781
5007
|
} else {
|
|
@@ -4786,7 +5012,7 @@ navMatches = [];
|
|
|
4786
5012
|
} else {
|
|
4787
5013
|
sessionNotifyEndpoint.setDiscordControlEnabled(isControlOn);
|
|
4788
5014
|
console.log(isControlOn
|
|
4789
|
-
? "Discord control enabled for this session.
|
|
5015
|
+
? "Discord control enabled for this sandbox session. Only authenticated allowlisted users can submit turns or explicit /sandbox controls. Run /discord control off to revoke remote control."
|
|
4790
5016
|
: "Discord control revoked for this session.");
|
|
4791
5017
|
}
|
|
4792
5018
|
}
|