@saadnvd1/agent-os 0.3.657 → 0.3.658
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.
|
@@ -34,6 +34,17 @@ export function writeLiveRecord(
|
|
|
34
34
|
fs.renameSync(tmp, recordPath(home));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// The recorded checkout the live server runs from, even when its process is
|
|
38
|
+
// gone (mid-restart).
|
|
39
|
+
export function liveCheckout(home = agentOsHome()): string | null {
|
|
40
|
+
try {
|
|
41
|
+
const rec = JSON.parse(fs.readFileSync(recordPath(home), "utf8"));
|
|
42
|
+
return typeof rec?.cwd === "string" && rec.cwd ? rec.cwd : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
export function readLiveRecord(home = agentOsHome()): InstanceRecord | null {
|
|
38
49
|
try {
|
|
39
50
|
const rec = JSON.parse(fs.readFileSync(recordPath(home), "utf8"));
|
|
@@ -160,7 +160,7 @@ Acting:
|
|
|
160
160
|
|
|
161
161
|
The MCP servers this workspace allows (Settings → Workspaces) are yours to use through their own tools, for reading and for additive, reversible changes. Anything outbound through them (sending a message, an email, a post) is a hard line: park it with \`ask_saad\`.
|
|
162
162
|
|
|
163
|
-
Act through these tools, not the shell: they're scoped to this workspace and braked. The shell runs anything without asking (Saad chose this on 2026-10-11), so use it to read and investigate yourself: \`gh pr list\`, \`gh run view --log-failed\`, \`gh api\` GETs, \`curl\`, \`sqlite3 -readonly\`, git in any repository, a skill's scripts, pipes and chains. Never use it to get around the tools. A deny list refuses, naming the tool to use instead: merging, closing or retargeting a PR (\`${TOOL_NAMES.sign_off}\`, \`${TOOL_NAMES.arm}\`, \`${TOOL_NAMES.land}\`, \`${TOOL_NAMES.drop}\`), \`git push\` (\`${TOOL_NAMES.quick_pr}\`), writing in a project repository or task worktree other than your own quick changes (\`${TOOL_NAMES.start_task}\`, \`${TOOL_NAMES.quick_change}\`), writing AgentOS's state (sqlite3 without \`-readonly\`, or files in ~/.agent-os outside your folder), stopping or restarting AgentOS or its tmux, and reading secret files (env files, keys, ~/.ssh, ~/.aws). Tokenvault is available through \`tv\`: use a token inside the command that needs it (e.g. \`FOO="\$(tv get <project> <desc>)" cmd\`), never print, echo or log a value, and never send one off the machine except to the service it's for. The list catches honest mistakes, not a determined bypass: a script or \`python -c\` could do any of these unseen, and doing so would break Saad's trust in every gate. \`aos notify "<text>"\` pushes a message to Saad's phone. Use it for what he'd want to see now and asked for: the morning report, a real milestone (a stack landed, a blocker only he can clear). Never for routine status: one a minute at most, and he reads the rest here. You can read files anywhere (Read, Grep, Glob), and write them (Write, Edit, NotebookEdit) anywhere except: a project repository or task worktree other than your own open quick changes' worktrees (start a task, or a quick change for a small edit, so the change goes through CI, review and scope), AgentOS's own state in ~/.agent-os outside your folder, secret files (.env, keys, ~/.ssh, ~/.aws, tokenvault and the like), and files that make something run: dotfiles and dot-folders at the top of your home (shell, git, Claude and tool config), ~/Library and /Library's launch agents, git's core.hooksPath folders, the folders on PATH, and any .git, .claude, .mcp.json, CLAUDE.md or AGENTS.md, which would hand you a shell or run code around the gates. Each write is checked before it runs, on the path with symlinks and \`..\` resolved; a refused one says why.
|
|
163
|
+
Act through these tools, not the shell: they're scoped to this workspace and braked. The shell runs anything without asking (Saad chose this on 2026-10-11), so use it to read and investigate yourself: \`gh pr list\`, \`gh run view --log-failed\`, \`gh api\` GETs, \`curl\`, \`sqlite3 -readonly\`, git in any repository, a skill's scripts, pipes and chains. Never use it to get around the tools. A deny list refuses, naming the tool to use instead: merging, closing or retargeting a PR (\`${TOOL_NAMES.sign_off}\`, \`${TOOL_NAMES.arm}\`, \`${TOOL_NAMES.land}\`, \`${TOOL_NAMES.drop}\`), \`git push\` (\`${TOOL_NAMES.quick_pr}\`), writing in a project repository or task worktree other than your own quick changes, except in a project checkout of your own workspace, where you may update and move the branch: \`git pull\` in any form, \`merge\`, \`reset\` (even \`--hard\`), \`checkout\`, \`switch\` and \`rebase\` (also as \`git -C <path>\`), then \`bundle install\`, \`npm install\` or \`bin/rails db:prepare\`. Commits, \`stash\`, \`clean\`, \`restore\` and file edits there still go through tasks and quick changes, and other workspaces' repositories, task worktrees and AgentOS's own live checkout (autodeploy deploys it) are refused for all of it are refused (\`${TOOL_NAMES.start_task}\`, \`${TOOL_NAMES.quick_change}\`), writing AgentOS's state (sqlite3 without \`-readonly\`, or files in ~/.agent-os outside your folder), stopping or restarting AgentOS or its tmux, and reading secret files (env files, keys, ~/.ssh, ~/.aws). Tokenvault is available through \`tv\`: use a token inside the command that needs it (e.g. \`FOO="\$(tv get <project> <desc>)" cmd\`), never print, echo or log a value, and never send one off the machine except to the service it's for. The list catches honest mistakes, not a determined bypass: a script or \`python -c\` could do any of these unseen, and doing so would break Saad's trust in every gate. \`aos notify "<text>"\` pushes a message to Saad's phone. Use it for what he'd want to see now and asked for: the morning report, a real milestone (a stack landed, a blocker only he can clear). Never for routine status: one a minute at most, and he reads the rest here. You can read files anywhere (Read, Grep, Glob), and write them (Write, Edit, NotebookEdit) anywhere except: a project repository or task worktree other than your own open quick changes' worktrees (start a task, or a quick change for a small edit, so the change goes through CI, review and scope), AgentOS's own state in ~/.agent-os outside your folder, secret files (.env, keys, ~/.ssh, ~/.aws, tokenvault and the like), and files that make something run: dotfiles and dot-folders at the top of your home (shell, git, Claude and tool config), ~/Library and /Library's launch agents, git's core.hooksPath folders, the folders on PATH, and any .git, .claude, .mcp.json, CLAUDE.md or AGENTS.md, which would hand you a shell or run code around the gates. Each write is checked before it runs, on the path with symlinks and \`..\` resolved; a refused one says why.
|
|
164
164
|
|
|
165
165
|
## Untrusted text
|
|
166
166
|
|
|
@@ -29,6 +29,10 @@ export interface ShellContext {
|
|
|
29
29
|
// Whether this path is under ~/.agent-os/audits, where reports go and
|
|
30
30
|
// nothing may be made executable.
|
|
31
31
|
inAudits?: (p: string) => Promise<boolean>;
|
|
32
|
+
// What a folder is for fast-forwarding: a project checkout of this
|
|
33
|
+
// workspace ("own"), the checkout AgentOS's live server and autodeploy run
|
|
34
|
+
// from ("live"), or anything else.
|
|
35
|
+
checkoutKind?: (dir: string) => Promise<"own" | "live" | "other">;
|
|
32
36
|
// AgentOS's server and the tmux servers it runs sessions in.
|
|
33
37
|
protectedPids: () => Promise<Set<number>>;
|
|
34
38
|
}
|
|
@@ -324,22 +328,40 @@ const GIT_WRITES = new Set([
|
|
|
324
328
|
|
|
325
329
|
const READ_ONLY_STASH = new Set(["list", "show"]);
|
|
326
330
|
|
|
331
|
+
// What an orchestrator may do to a project checkout of its own workspace:
|
|
332
|
+
// bring it up to date and move its branch (Saad, 2026-10-18). Commits, stash,
|
|
333
|
+
// clean and file edits still go through tasks and quick changes.
|
|
334
|
+
const CHECKOUT_UPDATES = new Set([
|
|
335
|
+
"pull",
|
|
336
|
+
"merge",
|
|
337
|
+
"reset",
|
|
338
|
+
"checkout",
|
|
339
|
+
"switch",
|
|
340
|
+
"rebase",
|
|
341
|
+
]);
|
|
342
|
+
|
|
327
343
|
async function gitRefusal(
|
|
328
|
-
|
|
344
|
+
inv: Invocation,
|
|
329
345
|
ctx: ShellContext,
|
|
330
346
|
cwd: string
|
|
331
347
|
): Promise<string | null> {
|
|
348
|
+
const args = inv.args;
|
|
349
|
+
// Every repository the command names: the folder after each -C, and each
|
|
350
|
+
// git dir and work tree, by option or environment.
|
|
351
|
+
const named = new Set<string>();
|
|
332
352
|
let dir = cwd;
|
|
333
353
|
let i = 0;
|
|
334
354
|
for (; i < args.length; i++) {
|
|
335
355
|
const a = args[i];
|
|
336
356
|
if (a === "-C" || a === "--git-dir" || a === "--work-tree") {
|
|
337
|
-
|
|
357
|
+
const to = path.resolve(dir, args[++i] ?? "");
|
|
358
|
+
if (a === "-C") dir = to;
|
|
359
|
+
named.add(to);
|
|
338
360
|
continue;
|
|
339
361
|
}
|
|
340
362
|
const eq = /^--(git-dir|work-tree)=(.*)$/.exec(a);
|
|
341
363
|
if (eq) {
|
|
342
|
-
|
|
364
|
+
named.add(path.resolve(dir, eq[2]));
|
|
343
365
|
continue;
|
|
344
366
|
}
|
|
345
367
|
if (a === "-c" || a === "--namespace" || a === "--exec-path") {
|
|
@@ -349,17 +371,28 @@ async function gitRefusal(
|
|
|
349
371
|
if (a.startsWith("-")) continue;
|
|
350
372
|
break;
|
|
351
373
|
}
|
|
374
|
+
named.add(dir);
|
|
375
|
+
for (const w of inv.cmd.words) {
|
|
376
|
+
const env = /^GIT_(?:DIR|WORK_TREE)=(.*)$/.exec(w);
|
|
377
|
+
if (env) named.add(path.resolve(cwd, env[1]));
|
|
378
|
+
}
|
|
352
379
|
const sub = args[i];
|
|
353
380
|
if (sub === "push")
|
|
354
381
|
return `Refused: ${T.quick_pr} pushes a quick change's branch and opens its PR, and tasks push their own; nothing pushes from the orchestrator's shell.`;
|
|
355
382
|
if (!GIT_WRITES.has(sub)) return null;
|
|
356
383
|
if (sub === "stash" && READ_ONLY_STASH.has(args[i + 1])) return null;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
384
|
+
for (const d of named) {
|
|
385
|
+
if (has(d))
|
|
386
|
+
return `Refused: git ${sub} in a folder only the shell knows (${show(d)}); name the folder.`;
|
|
387
|
+
if (!(await ctx.writeRefusal(d))) continue;
|
|
388
|
+
const kind = (await ctx.checkoutKind?.(d)) ?? "other";
|
|
389
|
+
const updates = CHECKOUT_UPDATES.has(sub);
|
|
390
|
+
if (kind === "live" && updates)
|
|
391
|
+
return `Refused: git ${sub} in ${d}. This is the checkout AgentOS's live server runs from; autodeploy deploys it (restart gap and health check), and changing it here would swap the code under the running server. For a manual deploy use scripts/redeploy.`;
|
|
392
|
+
if (kind === "own" && updates) continue;
|
|
393
|
+
return `Refused: git ${sub} in ${d}. Orchestrators don't change project checkouts except to update them: in a project checkout of your own workspace you may \`git pull\` (any form), \`merge\`, \`reset\`, \`checkout\`, \`switch\` and \`rebase\`. Commits, \`stash\`, \`clean\`, \`restore\` and file edits go through ${T.start_task} or ${T.quick_change}; other workspaces' repositories, task worktrees and AgentOS's live checkout are never changed from here. To see current code: \`git -C ${d} fetch\` then \`git -C ${d} show origin/main:<path>\` or \`git -C ${d} log origin/main\`.`;
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
363
396
|
}
|
|
364
397
|
|
|
365
398
|
// Commands whose every non-option argument is a file they change, and
|
|
@@ -557,7 +590,7 @@ export async function shellRefusal(
|
|
|
557
590
|
(inv.name === "gh" ? ghRefusal(inv.args) : null) ??
|
|
558
591
|
(inv.name === "curl" ? curlRefusal(inv.args) : null) ??
|
|
559
592
|
(inv.name === "aos" ? aosRefusal(inv.args) : null) ??
|
|
560
|
-
(inv.name === "git" ? await gitRefusal(inv
|
|
593
|
+
(inv.name === "git" ? await gitRefusal(inv, ctx, cwd) : null) ??
|
|
561
594
|
(inv.name === "sqlite3"
|
|
562
595
|
? await sqliteRefusal(inv, ctx, cwd, command)
|
|
563
596
|
: null) ??
|
|
@@ -24,6 +24,7 @@ import { orchestratorDir } from "./home";
|
|
|
24
24
|
import { LIMITS_FILE } from "./usage";
|
|
25
25
|
import { quickWorktrees } from "./quick-store";
|
|
26
26
|
import { shellRefusal } from "./shell-guard";
|
|
27
|
+
import { liveCheckout } from "../dev-instance/liveServer";
|
|
27
28
|
|
|
28
29
|
// The tools that write a file, and the argument naming it.
|
|
29
30
|
export const WRITE_TOOLS: Record<string, string> = {
|
|
@@ -398,10 +399,61 @@ async function commandRefusal(
|
|
|
398
399
|
: [];
|
|
399
400
|
return roots.some((r) => within(p, r));
|
|
400
401
|
},
|
|
402
|
+
checkoutKind: (dir) => checkoutKind(session, dir),
|
|
401
403
|
protectedPids,
|
|
402
404
|
});
|
|
403
405
|
}
|
|
404
406
|
|
|
407
|
+
// Whether a folder is a project checkout of the session's own workspace
|
|
408
|
+
// (where it may update and move the branch), a checkout AgentOS's live
|
|
409
|
+
// server or autodeploy runs from (never), or neither. The live ones are the
|
|
410
|
+
// server's folder, the one live-server.json recorded (known even while the
|
|
411
|
+
// server restarts), and any main checkout holding scripts/autodeploy, since
|
|
412
|
+
// that script deploys the folder it sits in (a task worktree has a .git
|
|
413
|
+
// file, not a folder).
|
|
414
|
+
export async function checkoutKind(
|
|
415
|
+
session: Session,
|
|
416
|
+
dir: string
|
|
417
|
+
): Promise<"own" | "live" | "other"> {
|
|
418
|
+
const real = await physicalPath(dir);
|
|
419
|
+
const roots = async (paths: (string | undefined)[]) =>
|
|
420
|
+
Promise.all(
|
|
421
|
+
paths
|
|
422
|
+
.filter((p): p is string => !!p)
|
|
423
|
+
.flatMap((p) => [path.resolve(p), p])
|
|
424
|
+
.map((p) => physicalPath(p))
|
|
425
|
+
);
|
|
426
|
+
const live = await roots([process.cwd(), liveCheckout() ?? undefined]);
|
|
427
|
+
if (live.some((r) => within(real, r)) || deploysFrom(real)) return "live";
|
|
428
|
+
if (!session.workspace_id) return "other";
|
|
429
|
+
const own = db
|
|
430
|
+
.prepare(
|
|
431
|
+
`SELECT working_directory AS dir FROM projects
|
|
432
|
+
WHERE workspace_id = ? AND is_uncategorized = 0 AND host_id = 'local'
|
|
433
|
+
UNION SELECT r.path FROM project_repositories r
|
|
434
|
+
JOIN projects p ON p.id = r.project_id
|
|
435
|
+
WHERE p.workspace_id = ? AND p.host_id = 'local'`
|
|
436
|
+
)
|
|
437
|
+
.all(session.workspace_id, session.workspace_id) as { dir: string }[];
|
|
438
|
+
const mine = await roots(own.map((p) => expandHome(p.dir)).filter(Boolean));
|
|
439
|
+
return mine.some((r) => within(real, r)) ? "own" : "other";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function deploysFrom(real: string): boolean {
|
|
443
|
+
for (let at = real; ; at = path.dirname(at)) {
|
|
444
|
+
try {
|
|
445
|
+
if (
|
|
446
|
+
fs.statSync(path.join(at, "scripts", "autodeploy")).isFile() &&
|
|
447
|
+
fs.statSync(path.join(at, ".git")).isDirectory()
|
|
448
|
+
)
|
|
449
|
+
return true;
|
|
450
|
+
} catch {
|
|
451
|
+
// Not here; look higher.
|
|
452
|
+
}
|
|
453
|
+
if (path.dirname(at) === at) return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
405
457
|
// AgentOS's server (whatever listens on its port), the tmux servers its
|
|
406
458
|
// sessions run in, and this worker. A lookup that fails refuses the kill.
|
|
407
459
|
async function protectedPids(): Promise<Set<number>> {
|