@nurix/nustack 0.23.17 → 0.23.19

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.
@@ -0,0 +1,131 @@
1
+ import { DEVICE_TOKEN_VARIABLE } from "../lib/records/environment.js";
2
+ import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
3
+ import { gitOutcome } from "../lib/session-engine/worktree.js";
4
+ import { ensureBinding, mountHarness, prepareCheckout, writeNustackShim } from "../lib/desktop-agents/checkout.js";
5
+ import { claudeArgs, claudeEnv, gitDirsOf, homeOf, realCheckout, runClaude, syncRecords } from "../lib/desktop-agents/claudeRun.js";
6
+ import { composePrompt } from "../lib/desktop-agents/prompt.js";
7
+ import { discardProposal, publishProposal, publishRun } from "../lib/desktop-agents/publish.js";
8
+ import { failedResult, parseProposalRequest, parseRunRequest, } from "../lib/desktop-agents/request.js";
9
+ async function readStdin() {
10
+ const chunks = [];
11
+ for await (const chunk of process.stdin)
12
+ chunks.push(chunk);
13
+ return Buffer.concat(chunks).toString("utf8");
14
+ }
15
+ async function readRequest(command, parse) {
16
+ let raw;
17
+ try {
18
+ raw = JSON.parse(await readStdin());
19
+ }
20
+ catch {
21
+ printJsonError(command, { code: "REQUEST_INVALID", message: "stdin must be one JSON request." });
22
+ return null;
23
+ }
24
+ const parsed = parse(raw);
25
+ if (!parsed.ok) {
26
+ printJsonError(command, { code: parsed.code, message: parsed.message });
27
+ return null;
28
+ }
29
+ return parsed.value;
30
+ }
31
+ function log(line) {
32
+ console.error(`desktop-agent: ${line}`);
33
+ }
34
+ function nustackHostOf(env) {
35
+ const url = env.NUSTACK_URL ?? env.NUSTACK_API_BASE_URL;
36
+ if (!url)
37
+ return "nustack.tech";
38
+ try {
39
+ return new URL(url).host;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function pullRequestBody(request, costCents) {
46
+ return [
47
+ `Agent: ${request.agent.label} (\`${request.agent.slug}\`)`,
48
+ `Firing session: ${request.sessionId ?? "local"}`,
49
+ `Engine: ${request.engine}`,
50
+ `Cost: $${(costCents / 100).toFixed(2)}`,
51
+ "",
52
+ "Opened by a NuStack desktop agent run.",
53
+ ].join("\n");
54
+ }
55
+ async function runAccepted(request) {
56
+ const env = process.env;
57
+ let prepared;
58
+ try {
59
+ prepared = await prepareCheckout(request, env);
60
+ }
61
+ catch (error) {
62
+ log(`checkout: ${error instanceof Error ? error.message : String(error)}`);
63
+ return failedResult("checkout_unbound", "none");
64
+ }
65
+ const { worktreePath, branch, base } = prepared;
66
+ if (!(await ensureBinding(worktreePath, request))) {
67
+ log(`checkout: ${worktreePath} is not bound to Project ${request.projectId}`);
68
+ return failedResult("checkout_unbound", "none");
69
+ }
70
+ const mountEnv = { ...env };
71
+ delete mountEnv[DEVICE_TOKEN_VARIABLE];
72
+ const mounted = await mountHarness(worktreePath, mountEnv);
73
+ if (!mounted.proceed) {
74
+ log(mounted.line);
75
+ return failedResult("harness_unmounted", "none");
76
+ }
77
+ if (mounted.line)
78
+ log(mounted.line);
79
+ const binDir = await writeNustackShim(env);
80
+ const checkout = realCheckout(worktreePath);
81
+ const commonDir = await gitOutcome(["rev-parse", "--git-common-dir"], worktreePath);
82
+ const gitDir = await gitOutcome(["rev-parse", "--git-dir"], worktreePath);
83
+ const dirs = gitDirsOf(commonDir.stdout, gitDir.stdout, worktreePath);
84
+ const inputs = { checkout, ...dirs, home: homeOf(env), nustackHost: nustackHostOf(env) };
85
+ const modelEnv = claudeEnv(request, env, binDir);
86
+ const outcome = await runClaude(request, claudeArgs(request, inputs, modelEnv), modelEnv, worktreePath, composePrompt(request));
87
+ if (request.sessionId !== null) {
88
+ const synced = await syncRecords(env, worktreePath);
89
+ log(`records sync exited ${synced.code ?? "unspawned"}`);
90
+ }
91
+ const shared = { vendorSessionId: outcome.vendorSessionId, costCents: outcome.costCents, engineCheck: outcome.engineCheck, mount: mounted.mount };
92
+ if (outcome.reason) {
93
+ await gitOutcome(["reset", "--hard"], worktreePath);
94
+ await gitOutcome(["checkout", "--detach"], worktreePath);
95
+ await gitOutcome(["branch", "-D", branch], worktreePath);
96
+ return failedResult(outcome.reason, mounted.mount, shared);
97
+ }
98
+ const published = await publishRun(request, worktreePath, branch, base, pullRequestBody(request, outcome.costCents), env);
99
+ return { outcome: "done", ...shared, result: published.result, pullRequestUrl: published.pullRequestUrl };
100
+ }
101
+ export async function runDesktopAgentRun() {
102
+ const request = await readRequest("desktop-agent run", parseRunRequest);
103
+ if (!request)
104
+ return;
105
+ printJsonSuccess("desktop-agent run", await runAccepted(request));
106
+ }
107
+ export async function runDesktopAgentPrompt() {
108
+ const request = await readRequest("desktop-agent prompt", parseRunRequest);
109
+ if (!request)
110
+ return;
111
+ printJsonSuccess("desktop-agent prompt", { prompt: composePrompt(request) });
112
+ }
113
+ export async function runDesktopAgentPublish() {
114
+ const request = await readRequest("desktop-agent publish", parseProposalRequest);
115
+ if (!request)
116
+ return;
117
+ const published = await publishProposal(request, process.env);
118
+ const result = published
119
+ ? { outcome: "done", vendorSessionId: null, costCents: 0, result: published.result, pullRequestUrl: published.pullRequestUrl, engineCheck: { apiKeySource: null, model: null }, mount: "none" }
120
+ : { outcome: "failed", vendorSessionId: null, costCents: 0, result: null, pullRequestUrl: null, engineCheck: { apiKeySource: null, model: null }, mount: "none" };
121
+ if (!published)
122
+ log(`publish: ${request.branch} could not be renamed or pushed`);
123
+ printJsonSuccess("desktop-agent publish", result);
124
+ }
125
+ export async function runDesktopAgentDiscard() {
126
+ const request = await readRequest("desktop-agent discard", parseProposalRequest);
127
+ if (!request)
128
+ return;
129
+ const discarded = await discardProposal(request);
130
+ printJsonSuccess("desktop-agent discard", { discarded });
131
+ }
@@ -0,0 +1,70 @@
1
+ import os from "node:os";
2
+ import { printJsonSuccess } from "../lib/json_output.js";
3
+ import { decideFromFacts, LANE_SENTENCES, readLaneFacts, SUBAGENT_MODEL_OVERRIDES_ROUTE } from "../lib/lane/decide.js";
4
+ import { haikuSentence } from "../lib/lane/mainModel.js";
5
+ import { installLane, LANE_AGENT_ROUTE, laneHooksInstalled, removeLane } from "../lib/lane/install.js";
6
+ function enableBlockersOf(facts) {
7
+ const blockers = [];
8
+ if (facts.mainModel.haikuClass && facts.mainModel.scope === "user") {
9
+ blockers.push({ code: "haiku-main-model", sentence: haikuSentence(facts.mainModel) });
10
+ }
11
+ if (SUBAGENT_MODEL_OVERRIDES_ROUTE && facts.subagentModel !== null)
12
+ blockers.push({ code: "subagent-model-override", sentence: LANE_SENTENCES["subagent-model-override"] });
13
+ return blockers;
14
+ }
15
+ function factsFor(cwd, interactive) {
16
+ return readLaneFacts({ env: process.env, cwd: cwd ?? process.cwd(), interactive, home: os.homedir() });
17
+ }
18
+ export function runLaneEnv(options) {
19
+ const decision = decideFromFacts(factsFor(options.cwd, options.nonInteractive !== true));
20
+ if (decision.state === "on") {
21
+ process.stdout.write(`ANTHROPIC_BASE_URL=${decision.baseUrl}\n`);
22
+ return;
23
+ }
24
+ if (decision.reason !== "unreleased" && decision.reason !== "switch-off") {
25
+ process.stderr.write(`NuStack lane off: ${decision.sentence}\n`);
26
+ }
27
+ }
28
+ export async function readLaneStatus(cwd, interactive) {
29
+ const facts = factsFor(cwd, interactive);
30
+ const decision = decideFromFacts(facts);
31
+ return {
32
+ released: facts.released,
33
+ switch: facts.switchOn,
34
+ proxy: facts.proxy.record === null ? null : { live: facts.proxy.live, port: facts.proxy.record.port },
35
+ credential: facts.credential.state === "on" ? facts.credential.credential : null,
36
+ mainModel: { value: facts.mainModel.value, source: facts.mainModel.source, haikuClass: facts.mainModel.haikuClass },
37
+ upstream: facts.upstream.state === "ok" ? facts.upstream.url : null,
38
+ routeMode: LANE_AGENT_ROUTE,
39
+ hooks: await laneHooksInstalled(),
40
+ decision: decision.state === "on"
41
+ ? { state: "on", reason: null, sentence: null }
42
+ : { state: "off", reason: decision.reason, sentence: decision.sentence },
43
+ enableBlockers: enableBlockersOf(facts),
44
+ };
45
+ }
46
+ export async function runLaneStatus(options) {
47
+ const status = await readLaneStatus(options.cwd, false);
48
+ if (options.json) {
49
+ printJsonSuccess("lane status", status);
50
+ return;
51
+ }
52
+ const lines = [
53
+ `Released: ${status.released ? "yes" : "no"} · Switch: ${status.switch ? "on" : "off"}`,
54
+ `Proxy: ${status.proxy === null ? "no record" : `${status.proxy.live ? "running" : "stopped"} on 127.0.0.1:${status.proxy.port}`}`,
55
+ `Credential: ${status.credential ?? "none the lane carries"} · Upstream: ${status.upstream ?? "refused"}`,
56
+ `Main model: ${status.mainModel.value ?? "Claude Code's default"}${status.mainModel.source ? ` (from ${status.mainModel.source})` : ""}`,
57
+ `Route mode: ${status.routeMode} · Hooks: Agent ${status.hooks.agent ? "installed" : "not installed"}, SubagentStop ${status.hooks.subagentStop ? "installed" : "not installed"}`,
58
+ `Decision: ${status.decision.state}${status.decision.reason ? ` (${status.decision.reason}): ${status.decision.sentence}` : ""}`,
59
+ ...status.enableBlockers.map((blocker) => `Blocks enabling: ${blocker.sentence}`),
60
+ ];
61
+ console.log(lines.join("\n"));
62
+ }
63
+ export async function runLaneInstall() {
64
+ const { changed } = await installLane();
65
+ console.log(changed ? "Installed the light-model lane's Agent and SubagentStop hooks." : "Installed the light-model lane's Agent and SubagentStop hooks (already installed — no change).");
66
+ }
67
+ export async function runLaneRemove() {
68
+ const { removed } = await removeLane();
69
+ console.log(removed ? "Removed the light-model lane's hooks." : "Removed the light-model lane's hooks (already absent — no change).");
70
+ }
@@ -0,0 +1,148 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { chmod, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { HARNESS_CODES } from "../harness/codes.js";
7
+ import { loadHarness } from "../harness/vendor.js";
8
+ import { projectJsonPath, readBindingMarker } from "../project_context.js";
9
+ import { gitOutcome, sessionsHome } from "../session-engine/worktree.js";
10
+ export const DESKTOP_AGENT_CHECKOUTS_DIR = "agents";
11
+ export const DESKTOP_AGENT_BIN_DIR = ".bin";
12
+ export const DESKTOP_AGENT_PROPOSAL_BRANCH_PREFIX = "nustack/proposal/";
13
+ export const DESKTOP_AGENT_EXCLUDE_START = "# nustack:desktop-agent:start";
14
+ export const DESKTOP_AGENT_EXCLUDE_END = "# nustack:desktop-agent:end";
15
+ const MARKER_RELATIVE = ".nustack/workspace.json";
16
+ const FETCH_TIMEOUT_MS = 10 * 60_000;
17
+ export function agentCheckoutsRoot(env = process.env) {
18
+ return path.join(sessionsHome(env), DESKTOP_AGENT_CHECKOUTS_DIR);
19
+ }
20
+ export function agentCheckoutPath(projectId, slug, env = process.env) {
21
+ return path.join(agentCheckoutsRoot(env), projectId, slug);
22
+ }
23
+ export async function runBranchName(slug, runId) {
24
+ const { AGENT_BRANCH_PREFIX } = await loadHarness("rules");
25
+ return `${AGENT_BRANCH_PREFIX}${slug}/${runId}`;
26
+ }
27
+ export function proposalBranchName(slug, runId) {
28
+ return `${DESKTOP_AGENT_PROPOSAL_BRANCH_PREFIX}${slug}/${runId}`;
29
+ }
30
+ export function agentBinDir(env = process.env) {
31
+ return path.join(agentCheckoutsRoot(env), DESKTOP_AGENT_BIN_DIR);
32
+ }
33
+ const CLI_ENTRY = fileURLToPath(new URL("../../index.js", import.meta.url));
34
+ export async function writeNustackShim(env = process.env) {
35
+ const dir = agentBinDir(env);
36
+ await mkdir(dir, { recursive: true });
37
+ const shim = path.join(dir, "nustack");
38
+ await writeFile(shim, `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(CLI_ENTRY)} "$@"\n`);
39
+ await chmod(shim, 0o755);
40
+ return dir;
41
+ }
42
+ function shellQuote(value) {
43
+ return `'${value.replace(/'/g, "'\\''")}'`;
44
+ }
45
+ async function gitText(args, cwd) {
46
+ const outcome = await gitOutcome(args, cwd);
47
+ return outcome.ok ? outcome.stdout : null;
48
+ }
49
+ async function gitMust(args, cwd, timeoutMs) {
50
+ const outcome = await gitOutcome(args, cwd, timeoutMs);
51
+ if (!outcome.ok)
52
+ throw new Error(`git ${args.join(" ")} failed: ${outcome.stderr || outcome.stdout}`);
53
+ }
54
+ export async function defaultBranchOf(checkoutPath) {
55
+ const head = await gitText(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], checkoutPath);
56
+ if (head && head.startsWith("origin/"))
57
+ return head.slice("origin/".length);
58
+ return "main";
59
+ }
60
+ export async function resolveBase(checkoutPath, prBase) {
61
+ if (prBase === "change") {
62
+ const current = await gitText(["rev-parse", "--abbrev-ref", "HEAD"], checkoutPath);
63
+ if (current && current !== "HEAD") {
64
+ const remote = await gitText(["ls-remote", "--heads", "origin", current], checkoutPath);
65
+ if (remote && remote.trim() !== "")
66
+ return current;
67
+ }
68
+ }
69
+ return defaultBranchOf(checkoutPath);
70
+ }
71
+ export async function prepareCheckout(request, env = process.env) {
72
+ const worktreePath = agentCheckoutPath(request.projectId, request.agent.slug, env);
73
+ if (!existsSync(path.join(worktreePath, ".git"))) {
74
+ await mkdir(path.dirname(worktreePath), { recursive: true });
75
+ await gitOutcome(["worktree", "prune"], request.checkoutPath);
76
+ await gitMust(["worktree", "add", "--detach", worktreePath], request.checkoutPath);
77
+ }
78
+ await gitMust(["fetch", "origin"], worktreePath, FETCH_TIMEOUT_MS);
79
+ await gitMust(["reset", "--hard"], worktreePath);
80
+ await gitMust(["clean", "-fd"], worktreePath);
81
+ const base = await resolveBase(request.checkoutPath, request.prBase);
82
+ const branch = await runBranchName(request.agent.slug, request.runId);
83
+ await gitMust(["checkout", "-B", branch, `origin/${base}`], worktreePath);
84
+ return { worktreePath, branch, base };
85
+ }
86
+ async function excludePathOf(worktreePath) {
87
+ const relative = await gitText(["rev-parse", "--git-path", "info/exclude"], worktreePath);
88
+ if (!relative)
89
+ return null;
90
+ return path.isAbsolute(relative) ? relative : path.join(worktreePath, relative);
91
+ }
92
+ async function writeExcludeBlock(worktreePath) {
93
+ const excludePath = await excludePathOf(worktreePath);
94
+ if (!excludePath)
95
+ return;
96
+ const held = existsSync(excludePath) ? await readFile(excludePath, "utf8") : "";
97
+ if (held.includes(DESKTOP_AGENT_EXCLUDE_START))
98
+ return;
99
+ await mkdir(path.dirname(excludePath), { recursive: true });
100
+ const block = [DESKTOP_AGENT_EXCLUDE_START, MARKER_RELATIVE, DESKTOP_AGENT_EXCLUDE_END].join("\n");
101
+ await writeFile(excludePath, `${held.replace(/\n*$/, held ? "\n" : "")}${block}\n`);
102
+ }
103
+ export async function ensureBinding(worktreePath, request) {
104
+ const own = await readBindingMarker(worktreePath);
105
+ if (own)
106
+ return own.projectId === request.projectId;
107
+ const person = await readBindingMarker(request.checkoutPath);
108
+ if (!person || person.projectId !== request.projectId)
109
+ return false;
110
+ await mkdir(path.dirname(projectJsonPath(worktreePath)), { recursive: true });
111
+ await copyFile(projectJsonPath(request.checkoutPath), projectJsonPath(worktreePath));
112
+ await writeExcludeBlock(worktreePath);
113
+ return true;
114
+ }
115
+ export function mountAnswerOf(lastLine) {
116
+ let envelope = null;
117
+ try {
118
+ envelope = lastLine ? JSON.parse(lastLine) : null;
119
+ }
120
+ catch {
121
+ envelope = null;
122
+ }
123
+ if (envelope?.ok === true)
124
+ return { proceed: true, mount: "nustack", line: null };
125
+ const code = typeof envelope?.error?.code === "string" ? envelope.error.code : "HARNESS_MOUNT_FAILED";
126
+ const message = typeof envelope?.error?.message === "string" ? envelope.error.message : "the mount printed no envelope";
127
+ if (code === HARNESS_CODES.HARNESS_UNAVAILABLE)
128
+ return { proceed: true, mount: "none", line: `harness: no version to mount yet (${message})` };
129
+ if (code === HARNESS_CODES.ETNA_MOUNT_PRESENT)
130
+ return { proceed: true, mount: "etna", line: `harness: etna's committed mount hook runs inside the session (${message})` };
131
+ return { proceed: false, code, line: `harness: mount failed ${code}: ${message}` };
132
+ }
133
+ export async function mountHarness(worktreePath, env) {
134
+ const lastLine = await new Promise((resolve) => {
135
+ const child = spawn(process.execPath, [CLI_ENTRY, "harness", "mount", worktreePath, "--json"], { cwd: worktreePath, env, stdio: ["ignore", "pipe", "ignore"] });
136
+ let stdout = "";
137
+ child.stdout.on("data", (chunk) => (stdout += chunk.toString("utf8")));
138
+ child.on("error", () => resolve(null));
139
+ child.on("close", () => {
140
+ const lines = stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "");
141
+ resolve(lines.at(-1) ?? null);
142
+ });
143
+ });
144
+ return mountAnswerOf(lastLine);
145
+ }
146
+ export function cliEntry() {
147
+ return CLI_ENTRY;
148
+ }
@@ -0,0 +1,265 @@
1
+ import { spawn } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { createInterface } from "node:readline";
6
+ import { DEVICE_TOKEN_VARIABLE, RECORD_CREDENTIAL_VARIABLE, SESSION_ID_VARIABLE } from "../records/environment.js";
7
+ import { cliEntry } from "./checkout.js";
8
+ export const DESKTOP_AGENT_NUSTACK_VERBS = ["nustack records emit *", "nustack obligation add *", "nustack obligation discharge *", "nustack backlog add *"];
9
+ const REVIEW_NUSTACK_VERBS = ["nustack obligation add *", "nustack obligation discharge *"];
10
+ const STRIPPED_FOR_MODEL = [DEVICE_TOKEN_VARIABLE, "NUSTACK_API_BASE_URL", SESSION_ID_VARIABLE, RECORD_CREDENTIAL_VARIABLE];
11
+ const LOCAL_MODEL_KEYS = ["ANTHROPIC_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "CLAUDE_CODE_SUBAGENT_MODEL"];
12
+ export const DESKTOP_AGENT_SANDBOX_DENY_READ = [
13
+ "~/.ssh",
14
+ "~/.aws",
15
+ "~/.config",
16
+ "~/Library",
17
+ "~/.claude-manager",
18
+ "~/.nustack/credentials.json",
19
+ "~/.nustack/device.json",
20
+ "~/.nustack/device_id",
21
+ "~/.nustack/local-engine.json",
22
+ "~/.nustack/recovery",
23
+ "~/.nustack/cache",
24
+ "~/Library/Application Support/co.nurix.nustack",
25
+ ];
26
+ export function absoluteRule(absolutePath) {
27
+ return `//${absolutePath.replace(/^\/+/, "")}`;
28
+ }
29
+ export function readTools(checkout) {
30
+ const scope = `${absoluteRule(checkout)}/**`;
31
+ return [`Read(${scope})`, `Glob(${scope})`, `Grep(${scope})`];
32
+ }
33
+ const GIT_READ_FORMS = ["Bash(git status)", "Bash(git status *)", "Bash(git diff)", "Bash(git diff *)", "Bash(git log)", "Bash(git log *)"];
34
+ export function allowedTools(checkout) {
35
+ const scope = `${absoluteRule(checkout)}/**`;
36
+ return [
37
+ ...readTools(checkout),
38
+ `Edit(${scope})`,
39
+ `Write(${scope})`,
40
+ ...GIT_READ_FORMS,
41
+ "Bash(git add *)",
42
+ "Bash(git commit -m *)",
43
+ ...DESKTOP_AGENT_NUSTACK_VERBS.map((verb) => `Bash(${verb})`),
44
+ ];
45
+ }
46
+ export function reviewTools(checkout) {
47
+ return [...readTools(checkout), ...GIT_READ_FORMS, ...REVIEW_NUSTACK_VERBS.map((verb) => `Bash(${verb})`)];
48
+ }
49
+ export function deniedTools(checkout) {
50
+ const gitDir = `${absoluteRule(checkout)}/.git/**`;
51
+ return [
52
+ `Edit(${gitDir})`,
53
+ `Write(${gitDir})`,
54
+ "Bash(git push *)",
55
+ "Bash(git -C *)",
56
+ "Bash(git -c *)",
57
+ "Bash(git --git-dir*)",
58
+ "Bash(git --work-tree*)",
59
+ "Bash(git diff --no-index*)",
60
+ "Bash(git commit -F*)",
61
+ "Bash(git commit --file*)",
62
+ "Bash(git commit -t*)",
63
+ "Bash(git commit --template*)",
64
+ "Bash(git * --output*)",
65
+ "Bash(nustack records list*)",
66
+ "Bash(nustack obligation list*)",
67
+ "Read(~/.ssh/**)",
68
+ "Read(~/.aws/**)",
69
+ "Read(~/.config/**)",
70
+ "Read(~/Library/**)",
71
+ "Read(~/.claude-manager/**)",
72
+ "Read(~/.nustack/*.db)",
73
+ "Read(~/.nustack/credentials.json)",
74
+ "Read(~/.nustack/device.json)",
75
+ "Read(~/.nustack/device_id)",
76
+ "Read(~/.nustack/local-engine.json)",
77
+ ];
78
+ }
79
+ export function realCheckout(checkout) {
80
+ try {
81
+ return realpathSync(checkout);
82
+ }
83
+ catch {
84
+ return checkout;
85
+ }
86
+ }
87
+ export function sandboxSettings(inputs) {
88
+ const home = realCheckout(inputs.home);
89
+ const nustackHome = path.join(home, ".nustack");
90
+ const expand = (entry) => (entry.startsWith("~/") ? path.join(home, entry.slice(2)) : entry);
91
+ return {
92
+ sandbox: {
93
+ enabled: true,
94
+ failIfUnavailable: true,
95
+ allowUnsandboxedCommands: false,
96
+ autoAllowBashIfSandboxed: false,
97
+ filesystem: {
98
+ allowWrite: [
99
+ inputs.checkout,
100
+ path.join(inputs.gitCommonDir, "objects"),
101
+ path.join(inputs.gitCommonDir, "refs"),
102
+ path.join(inputs.gitCommonDir, "logs"),
103
+ inputs.worktreeGitDir,
104
+ path.join(nustackHome, "state.db"),
105
+ path.join(nustackHome, "state.db-wal"),
106
+ path.join(nustackHome, "state.db-shm"),
107
+ path.join(nustackHome, "locks"),
108
+ path.join(nustackHome, "session-events"),
109
+ ],
110
+ denyRead: DESKTOP_AGENT_SANDBOX_DENY_READ.map(expand),
111
+ },
112
+ network: { allowedDomains: inputs.nustackHost ? [inputs.nustackHost] : [] },
113
+ },
114
+ };
115
+ }
116
+ export function runSettings(request, inputs, env) {
117
+ const settings = sandboxSettings(inputs);
118
+ if (request.engine === "local_model") {
119
+ const block = {};
120
+ for (const key of LOCAL_MODEL_KEYS) {
121
+ const value = env[key];
122
+ if (value)
123
+ block[key] = value;
124
+ }
125
+ settings.env = block;
126
+ }
127
+ return settings;
128
+ }
129
+ export function modelOf(request, env) {
130
+ if (request.engine === "local_model")
131
+ return env.ANTHROPIC_MODEL ?? request.agent.model;
132
+ return request.agent.model;
133
+ }
134
+ export function claudeArgs(request, inputs, env) {
135
+ const allowed = request.toolProfile === "review" ? reviewTools(inputs.checkout) : allowedTools(inputs.checkout);
136
+ const args = [
137
+ "-p",
138
+ "--no-session-persistence",
139
+ "--output-format",
140
+ "stream-json",
141
+ "--verbose",
142
+ "--permission-mode",
143
+ "dontAsk",
144
+ "--allowedTools",
145
+ allowed.join(","),
146
+ "--disallowedTools",
147
+ deniedTools(inputs.checkout).join(","),
148
+ ];
149
+ const model = modelOf(request, env);
150
+ if (model)
151
+ args.push("--model", model);
152
+ args.push("--settings", JSON.stringify(runSettings(request, inputs, env)));
153
+ if (request.budgetCents !== null)
154
+ args.push("--max-budget-usd", (request.budgetCents / 100).toFixed(2));
155
+ return args;
156
+ }
157
+ export function claudeEnv(request, inherited, binDir) {
158
+ const env = { ...inherited };
159
+ for (const name of STRIPPED_FOR_MODEL)
160
+ delete env[name];
161
+ if (request.sessionId !== null) {
162
+ env[SESSION_ID_VARIABLE] = request.sessionId;
163
+ env[RECORD_CREDENTIAL_VARIABLE] = "device";
164
+ }
165
+ env.PATH = [binDir, inherited.PATH ?? ""].filter((part) => part !== "").join(path.delimiter);
166
+ return env;
167
+ }
168
+ export function engineFits(request, init, env) {
169
+ if (request.engine === "api_key")
170
+ return init.apiKeySource === null || init.apiKeySource === "ANTHROPIC_API_KEY";
171
+ if (request.engine === "seat")
172
+ return init.apiKeySource === null || init.apiKeySource === "none";
173
+ const expected = modelOf(request, env);
174
+ return init.model === null || expected === null || init.model === expected;
175
+ }
176
+ const KILL_GRACE_MS = 3_000;
177
+ export function runClaude(request, args, env, cwd, prompt) {
178
+ return new Promise((resolve) => {
179
+ const child = spawn(env.NUSTACK_CLAUDE_BIN ?? "claude", args, { cwd, env, stdio: ["pipe", "pipe", "pipe"], detached: true });
180
+ let vendorSessionId = null;
181
+ let costCents = 0;
182
+ let sawResult = false;
183
+ let isResultError = false;
184
+ let killedFor = null;
185
+ const engineCheck = { apiKeySource: null, model: null };
186
+ let settled = false;
187
+ const killGroup = (reason) => {
188
+ if (killedFor)
189
+ return;
190
+ killedFor = reason;
191
+ const signal = (sig) => {
192
+ try {
193
+ if (child.pid)
194
+ process.kill(-child.pid, sig);
195
+ }
196
+ catch {
197
+ }
198
+ };
199
+ signal("SIGTERM");
200
+ setTimeout(() => signal("SIGKILL"), KILL_GRACE_MS).unref();
201
+ };
202
+ const wall = setTimeout(() => killGroup("wall_clock"), request.wallSeconds * 1000);
203
+ const finish = (code) => {
204
+ if (settled)
205
+ return;
206
+ settled = true;
207
+ clearTimeout(wall);
208
+ let reason = killedFor;
209
+ if (!reason && (code !== 0 || !sawResult || isResultError))
210
+ reason = "model_failed";
211
+ resolve({ reason, vendorSessionId, costCents, engineCheck });
212
+ };
213
+ child.on("error", () => finish(null));
214
+ child.on("close", (code) => finish(code));
215
+ child.stderr.on("data", () => {
216
+ });
217
+ createInterface({ input: child.stdout }).on("line", (line) => {
218
+ let frame;
219
+ try {
220
+ const parsed = JSON.parse(line);
221
+ if (typeof parsed !== "object" || parsed === null)
222
+ return;
223
+ frame = parsed;
224
+ }
225
+ catch {
226
+ return;
227
+ }
228
+ if (frame.type === "system" && frame.subtype === "init") {
229
+ if (typeof frame.session_id === "string")
230
+ vendorSessionId = frame.session_id;
231
+ engineCheck.apiKeySource = typeof frame.apiKeySource === "string" ? frame.apiKeySource : null;
232
+ engineCheck.model = typeof frame.model === "string" ? frame.model : null;
233
+ if (!engineFits(request, engineCheck, env))
234
+ killGroup("engine_mismatch");
235
+ }
236
+ if (frame.type === "result") {
237
+ sawResult = true;
238
+ isResultError = frame.is_error === true;
239
+ if (typeof frame.total_cost_usd === "number")
240
+ costCents = Math.round(frame.total_cost_usd * 100);
241
+ if (typeof frame.session_id === "string" && !vendorSessionId)
242
+ vendorSessionId = frame.session_id;
243
+ }
244
+ });
245
+ child.stdin.on("error", () => {
246
+ });
247
+ child.stdin.end(prompt);
248
+ });
249
+ }
250
+ export function gitDirsOf(gitCommonDirOutput, gitDirOutput, worktree) {
251
+ const absolute = (value) => realCheckout(path.isAbsolute(value) ? value : path.join(worktree, value));
252
+ return { gitCommonDir: absolute(gitCommonDirOutput), worktreeGitDir: absolute(gitDirOutput) };
253
+ }
254
+ export function syncRecords(env, cwd) {
255
+ return new Promise((resolve) => {
256
+ const child = spawn(process.execPath, [cliEntry(), "records", "sync", "--json"], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
257
+ let output = "";
258
+ child.stdout.on("data", (chunk) => (output += chunk.toString("utf8")));
259
+ child.on("error", () => resolve({ code: null, output }));
260
+ child.on("close", (code) => resolve({ code, output: output.trim() }));
261
+ });
262
+ }
263
+ export function homeOf(env) {
264
+ return env.HOME ?? os.homedir();
265
+ }
@@ -0,0 +1,47 @@
1
+ export const LOCAL_ROWS_MAX = 50;
2
+ export const LOCAL_DISMISSALS_MAX = 20;
3
+ export const PREFILLED_PROMPT_MAX_CHARS = 8_000;
4
+ export const LOCAL_TASK_TEMPLATES = {
5
+ distillation: "Distill the device-kept memory rows below into durable project memory. Edit the memory documents in this checkout, then commit. Nothing is pushed; the person approves the proposal.",
6
+ boundary_security_reviewer: "Review the open device-kept security findings below against this checkout. For each, discharge the obligation with your verdict through `nustack obligation discharge`, weighing the past dismissals. Do not edit or commit.",
7
+ };
8
+ const BRANCH_RULE = "Work on the current branch and commit here with `git commit -m`; never push. Write records only through the `nustack` record verbs.";
9
+ function section(title, body) {
10
+ if (!body || body.trim() === "")
11
+ return null;
12
+ return `## ${title}\n\n${body.trim()}`;
13
+ }
14
+ function rowsSection(title, rows, max) {
15
+ if (!rows || rows.length === 0)
16
+ return null;
17
+ const kept = rows.slice(0, max).map((row) => `- ${JSON.stringify(row)}`);
18
+ return `## ${title}\n\n${kept.join("\n")}`;
19
+ }
20
+ function taskOf(request) {
21
+ const mandate = request.agent.mandate?.trim();
22
+ const task = request.task.trim();
23
+ if (mandate && task.startsWith(mandate))
24
+ return task.slice(mandate.length).trim();
25
+ if (task !== "")
26
+ return request.task;
27
+ const template = LOCAL_TASK_TEMPLATES[request.agent.slug];
28
+ return request.localRows !== undefined && template ? template : request.task;
29
+ }
30
+ function bodySections(request) {
31
+ const isReviewer = request.agent.slug === "boundary_security_reviewer";
32
+ return [
33
+ section("Mandate", request.agent.mandate),
34
+ section("Task", taskOf(request)),
35
+ section("Branch rule", BRANCH_RULE),
36
+ rowsSection("Device rows", request.localRows, LOCAL_ROWS_MAX),
37
+ isReviewer ? rowsSection("Past device dismissals", request.dismissals, LOCAL_DISMISSALS_MAX) : null,
38
+ ].filter((part) => part !== null);
39
+ }
40
+ export function composePrompt(request) {
41
+ const body = bodySections(request).join("\n\n");
42
+ const instructions = section("Instructions", request.instructions);
43
+ const whole = instructions ? `${instructions}\n\n${body}` : body;
44
+ if (whole.length <= PREFILLED_PROMPT_MAX_CHARS)
45
+ return whole;
46
+ return body.slice(0, PREFILLED_PROMPT_MAX_CHARS);
47
+ }