From 86b029548fa7a0520974520dacf2ff32a117f5d7 Mon Sep 17 00:00:00 2001 From: Vikas Singhal Date: Tue, 22 Sep 2026 22:16:28 +0530 Subject: [PATCH] fix(sessions): fence agent git discovery at the agents folder (v0.448.6) A failed cd + unchained git reset in an agent folder walked up into the agent-os checkout and replaced the product source with another repo. Launch now exports GIT_CEILING_DIRECTORIES = the agents folder. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 10 +++++ package-lock.json | 4 +- package.json | 4 +- scripts/git-ceiling-test.cjs | 77 ++++++++++++++++++++++++++++++++++++ src/terminal.ts | 8 ++++ 5 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 scripts/git-ceiling-test.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0fde5d..e1474c34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ new version heading in the same commit. ## [Unreleased] +## [0.448.6] - 2026-09-22 +### Fixed +- **An agent's git commands can no longer reach the Agentric checkout.** An agent's folder usually isn't a + repo, so git walks up, and where the data home sits inside the product checkout it lands in agent-os + itself. On globex an engineer run did `cd ` followed, unchained, by + `git remote set-url origin …client-app` + `git reset --hard`. The cd failed, and the product's own source + was replaced by another repo (`terminal/` gone, so every later session crashed on launch, for 80 minutes). + Every launch now exports `GIT_CEILING_DIRECTORIES` = the agents folder. Repos inside an agent's folder + are unaffected. Pinned by `scripts/git-ceiling-test.cjs`. + ## [0.448.5] - 2026-09-22 ### Fixed - **Two synchronous paths could freeze a whole tenant's server.** Both hit globex on 2026-09-22: `/health` diff --git a/package-lock.json b/package-lock.json index 7901ff19..0e40f818 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.448.5", + "version": "0.448.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.448.5", + "version": "0.448.6", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index ac3e1226..baaab297 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.448.5", + "version": "0.448.6", "description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.", "license": "MIT", "type": "commonjs", @@ -27,7 +27,7 @@ "check-deps": "bash scripts/install-deps.sh --check", "dev": "ts-node src/cli.ts serve", "demo:dev": "ts-node src/demo.ts", - "test:governance": "node scripts/version-sync-test.cjs && node scripts/event-loop-freeze-test.cjs && node scripts/governance-conformance.cjs && node scripts/tier-a-policy-test.cjs && node scripts/policy-baseline-test.cjs && node scripts/heredoc-intent-test.cjs && node scripts/capability-registry-test.cjs && node scripts/composio-envelope-test.cjs && node scripts/composio-identity-test.cjs && node scripts/idle-reaper-test.cjs && node scripts/dm-continuity-test.cjs && node scripts/telegram-dm-lane-test.cjs && node scripts/cli-link-origin-test.cjs && node scripts/alert-staleness-test.cjs && node scripts/run-as-identity-test.cjs && node scripts/email-identity-guard-test.cjs && node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-usage-refresh-test.cjs && node scripts/keychain-credential-test.cjs && node scripts/credential-preflight-test.cjs && node scripts/capacity-queue-test.cjs && node scripts/runtime-login-test.cjs && node scripts/rotate-on-reload-test.cjs && node scripts/headless-resumable-test.cjs && node scripts/session-revive-gates-test.cjs && node scripts/session-pause-test.cjs && node scripts/make-live-lock-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs && node scripts/output-style-test.cjs && node scripts/session-cost-test.cjs && node scripts/chain-model-test.cjs && node scripts/task-workers-test.cjs && node scripts/tuning-patch-test.cjs && node scripts/task-runs-test.cjs && node scripts/task-pr-links-test.cjs && node scripts/task-draft-delete-test.cjs && node scripts/task-discussion-delivery-test.cjs && node scripts/task-resume-test.cjs && node scripts/task-unblock-test.cjs && node scripts/audience-session-access-test.cjs && node scripts/warm-chat-test.cjs && node scripts/poke-warm-caller-test.cjs && node scripts/wakeup-queue-test.cjs && node scripts/stranded-human-stop-test.cjs && node scripts/inject-submit-test.cjs && node scripts/blocked-routing-test.cjs && node scripts/self-dispatch-guard-test.cjs && node scripts/task-proposals-test.cjs && node scripts/npm-boundary-test.cjs && node scripts/agent-edit-guard-test.cjs && node scripts/per-agent-context-test.cjs && node scripts/goal-update-guard-test.cjs && node scripts/insights-signal-test.cjs && node scripts/outcome-derivation-test.cjs && node scripts/episode-quality-test.cjs && node scripts/memory-upkeep-test.cjs && node scripts/automem-health-test.cjs && node scripts/memory-store-switch-test.cjs && node scripts/memory-preload-test.cjs && node scripts/turn-lifecycle-test.cjs && node scripts/resume-seed-test.cjs && node scripts/outcome-vocabulary-test.cjs && node scripts/skill-presets-test.cjs && node scripts/skill-edit-proposal-test.cjs && node scripts/notify-hook-route-test.cjs && node scripts/review-notify-test.cjs && node scripts/turn-idle-background-guard-test.cjs && node scripts/waiting-brief-test.cjs && node scripts/runtime-death-alert-test.cjs && node scripts/github-per-member-test.cjs && node scripts/github-multi-org-test.cjs && node scripts/card-measurement-test.cjs && node scripts/scheduler-admission-test.cjs && node scripts/tick-liveness-test.cjs && node scripts/audit-mirror-test.cjs && node scripts/request-metrics-test.cjs && node scripts/tool-usage-test.cjs && node scripts/sessions-list-perf-test.cjs && node scripts/summarizer-degradation-test.cjs && node scripts/agent-history-scope-test.cjs && node scripts/webhook-ingress-test.cjs && node scripts/slack-content-filter-test.cjs && node scripts/slack-ingress-test.cjs && node scripts/discord-ingress-test.cjs && node scripts/chat-attachments-test.cjs && node scripts/clickup-task-bridge-test.cjs && node scripts/agentric-commands-test.cjs && node scripts/whats-new-test.cjs && node scripts/opencode-gate-test.cjs && node scripts/protected-path-guard-test.cjs && node scripts/attach-grace-test.cjs && node scripts/attach-file-liveness-test.cjs && node scripts/feed-smoke.cjs && node scripts/activity-classify-test.cjs && node scripts/goal-room-test.cjs && node scripts/secret-rotation-test.cjs && node scripts/update-watch-test.cjs && node scripts/runtime-update-watch-test.cjs && node scripts/setup-wizard-test.cjs && node scripts/md-pdf-test.cjs && node scripts/proposal-surfacing-test.cjs && node scripts/process-janitor-test.cjs && node scripts/detached-work-steer-test.cjs && node scripts/statusline-install-test.cjs && node scripts/docs-create-agent-test.cjs && node scripts/agent-stats-rollup-test.cjs && node scripts/task-discussion-rollup-test.cjs && node scripts/session-insights-stamp-test.cjs && node scripts/loop-stall-attribution-test.cjs && node scripts/session-progress-test.cjs && node scripts/drift-nudge-test.cjs && node scripts/goal-metric-review-test.cjs && node scripts/capability-gap-test.cjs && node scripts/workflow-proposal-test.cjs && node scripts/automation-edit-proposal-test.cjs && node scripts/app-card-close-test.cjs", + "test:governance": "node scripts/version-sync-test.cjs && node scripts/event-loop-freeze-test.cjs && node scripts/git-ceiling-test.cjs && node scripts/governance-conformance.cjs && node scripts/tier-a-policy-test.cjs && node scripts/policy-baseline-test.cjs && node scripts/heredoc-intent-test.cjs && node scripts/capability-registry-test.cjs && node scripts/composio-envelope-test.cjs && node scripts/composio-identity-test.cjs && node scripts/idle-reaper-test.cjs && node scripts/dm-continuity-test.cjs && node scripts/telegram-dm-lane-test.cjs && node scripts/cli-link-origin-test.cjs && node scripts/alert-staleness-test.cjs && node scripts/run-as-identity-test.cjs && node scripts/email-identity-guard-test.cjs && node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-usage-refresh-test.cjs && node scripts/keychain-credential-test.cjs && node scripts/credential-preflight-test.cjs && node scripts/capacity-queue-test.cjs && node scripts/runtime-login-test.cjs && node scripts/rotate-on-reload-test.cjs && node scripts/headless-resumable-test.cjs && node scripts/session-revive-gates-test.cjs && node scripts/session-pause-test.cjs && node scripts/make-live-lock-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs && node scripts/output-style-test.cjs && node scripts/session-cost-test.cjs && node scripts/chain-model-test.cjs && node scripts/task-workers-test.cjs && node scripts/tuning-patch-test.cjs && node scripts/task-runs-test.cjs && node scripts/task-pr-links-test.cjs && node scripts/task-draft-delete-test.cjs && node scripts/task-discussion-delivery-test.cjs && node scripts/task-resume-test.cjs && node scripts/task-unblock-test.cjs && node scripts/audience-session-access-test.cjs && node scripts/warm-chat-test.cjs && node scripts/poke-warm-caller-test.cjs && node scripts/wakeup-queue-test.cjs && node scripts/stranded-human-stop-test.cjs && node scripts/inject-submit-test.cjs && node scripts/blocked-routing-test.cjs && node scripts/self-dispatch-guard-test.cjs && node scripts/task-proposals-test.cjs && node scripts/npm-boundary-test.cjs && node scripts/agent-edit-guard-test.cjs && node scripts/per-agent-context-test.cjs && node scripts/goal-update-guard-test.cjs && node scripts/insights-signal-test.cjs && node scripts/outcome-derivation-test.cjs && node scripts/episode-quality-test.cjs && node scripts/memory-upkeep-test.cjs && node scripts/automem-health-test.cjs && node scripts/memory-store-switch-test.cjs && node scripts/memory-preload-test.cjs && node scripts/turn-lifecycle-test.cjs && node scripts/resume-seed-test.cjs && node scripts/outcome-vocabulary-test.cjs && node scripts/skill-presets-test.cjs && node scripts/skill-edit-proposal-test.cjs && node scripts/notify-hook-route-test.cjs && node scripts/review-notify-test.cjs && node scripts/turn-idle-background-guard-test.cjs && node scripts/waiting-brief-test.cjs && node scripts/runtime-death-alert-test.cjs && node scripts/github-per-member-test.cjs && node scripts/github-multi-org-test.cjs && node scripts/card-measurement-test.cjs && node scripts/scheduler-admission-test.cjs && node scripts/tick-liveness-test.cjs && node scripts/audit-mirror-test.cjs && node scripts/request-metrics-test.cjs && node scripts/tool-usage-test.cjs && node scripts/sessions-list-perf-test.cjs && node scripts/summarizer-degradation-test.cjs && node scripts/agent-history-scope-test.cjs && node scripts/webhook-ingress-test.cjs && node scripts/slack-content-filter-test.cjs && node scripts/slack-ingress-test.cjs && node scripts/discord-ingress-test.cjs && node scripts/chat-attachments-test.cjs && node scripts/clickup-task-bridge-test.cjs && node scripts/agentric-commands-test.cjs && node scripts/whats-new-test.cjs && node scripts/opencode-gate-test.cjs && node scripts/protected-path-guard-test.cjs && node scripts/attach-grace-test.cjs && node scripts/attach-file-liveness-test.cjs && node scripts/feed-smoke.cjs && node scripts/activity-classify-test.cjs && node scripts/goal-room-test.cjs && node scripts/secret-rotation-test.cjs && node scripts/update-watch-test.cjs && node scripts/runtime-update-watch-test.cjs && node scripts/setup-wizard-test.cjs && node scripts/md-pdf-test.cjs && node scripts/proposal-surfacing-test.cjs && node scripts/process-janitor-test.cjs && node scripts/detached-work-steer-test.cjs && node scripts/statusline-install-test.cjs && node scripts/docs-create-agent-test.cjs && node scripts/agent-stats-rollup-test.cjs && node scripts/task-discussion-rollup-test.cjs && node scripts/session-insights-stamp-test.cjs && node scripts/loop-stall-attribution-test.cjs && node scripts/session-progress-test.cjs && node scripts/drift-nudge-test.cjs && node scripts/goal-metric-review-test.cjs && node scripts/capability-gap-test.cjs && node scripts/workflow-proposal-test.cjs && node scripts/automation-edit-proposal-test.cjs && node scripts/app-card-close-test.cjs", "test:alert-staleness": "node scripts/alert-staleness-test.cjs", "test:deps": "node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-login-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs", "test:dm-continuity": "node scripts/dm-continuity-test.cjs", diff --git a/scripts/git-ceiling-test.cjs b/scripts/git-ceiling-test.cjs new file mode 100644 index 00000000..42c9ae71 --- /dev/null +++ b/scripts/git-ceiling-test.cjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +/* Git discovery is fenced at the agents folder. + * + * globex 2026-09-22: an engineer run did `cd ` and then, unchained, + * `git remote set-url origin …client-app` + `git reset --hard origin/`. The cd failed, git walked + * up from the agent's folder to the agent-os checkout the data home lived in, and replaced the product's + * own source with another repo — `terminal/` gone, every later session crashed on launch. + * + * Pins: every launch exports GIT_CEILING_DIRECTORIES = the agents folder; with it, git run in the agent's + * folder finds NO repo even when an ancestor is one (and without it, it would — the test proves the walk + * is real); a repo INSIDE the agent's folder still works. Isolated home; no tmux or claude needed. */ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const ROOT = path.resolve(__dirname, '..'); +const HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'aos-git-ceiling-test-')); +process.env.AGENT_OS_HOME = HOME; +process.env.AGENT_OS_TENANT = 'testco'; +process.env.AOS_NO_TTYD = '1'; +process.env.CLAUDE_CONFIG_DIR = path.join(HOME, 'claude-config'); +fs.mkdirSync(process.env.CLAUDE_CONFIG_DIR, { recursive: true }); +delete process.env.AGENT_OS_SECRET_KEY; + +let pass = 0, fail = 0; +const assert = (c, name, d) => c ? (pass++, console.log(` \x1b[32m✓\x1b[0m ${name}`)) : (fail++, console.log(` \x1b[31m✗ ${name}\x1b[0m${d ? ' — ' + d : ''}`)); + +const { loadAgentOS } = require(path.join(ROOT, 'dist/kernel.js')); +const { TerminalManager } = require(path.join(ROOT, 'dist/terminal.js')); +const aos = loadAgentOS(); +const tm = new TerminalManager(aos, 'http://127.0.0.1:0', path.join(HOME, 'tmux.sock')); + +let spawned; +const live = new Set(); +tm.backend.aliveNames = () => new Set(live); +tm.backend.spawn = (_s, o) => { spawned = o; live.add(o.tmuxName); }; +tm.backend.kill = (_s, t) => { live.delete(t); }; +tm.backend.capturePane = () => ''; +tm.backend.hasClient = () => false; + +// Only git's own env should decide discovery in these probes. +const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('GIT_'))); +const git = (cwd, extra) => spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, env: { ...cleanEnv, ...extra }, encoding: 'utf8' }); + +(async () => { + const agent = aos.listAgents ? (aos.listAgents()[0]?.id) : undefined; + const AGENT = agent || 'agent-author'; + tm.createSession(AGENT, 'probe', 'noop', 'automation:a1', true); + await new Promise((r) => setTimeout(r, 50)); + + const env = spawned?.env || {}; + const agentDir = env.AGENT_DIR; + assert(!!agentDir, 'the launch carries AGENT_DIR', JSON.stringify(Object.keys(env)).slice(0, 200)); + assert(env.GIT_CEILING_DIRECTORIES === fs.realpathSync(path.dirname(agentDir)), + 'GIT_CEILING_DIRECTORIES is the (real) agents folder', `${env.GIT_CEILING_DIRECTORIES} vs ${agentDir}`); + + // Model the live layout: the data home inside a product checkout. Make the home's PARENT chain a repo. + const product = fs.realpathSync(HOME); + spawnSync('git', ['init', '-q', product]); + const without = git(agentDir, {}); + assert(without.status === 0 && without.stdout.trim() === product, + 'control: WITHOUT the fence, git in the agent folder resolves to the enclosing checkout', without.stderr); + const withFence = git(agentDir, { GIT_CEILING_DIRECTORIES: env.GIT_CEILING_DIRECTORIES }); + assert(withFence.status !== 0 && /not a git repository/.test(withFence.stderr), + 'WITH the fence, git in the agent folder finds no repo — a stray reset cannot reach the product', withFence.stdout); + + const inner = path.join(agentDir, 'repos', 'client-app'); + fs.mkdirSync(inner, { recursive: true }); + spawnSync('git', ['init', '-q', inner]); + const own = git(inner, { GIT_CEILING_DIRECTORIES: env.GIT_CEILING_DIRECTORIES }); + assert(own.status === 0 && own.stdout.trim() === fs.realpathSync(inner), 'a repo inside the agent folder still works', own.stderr); + + console.log(`\ngit-ceiling-test: ${pass} passed, ${fail} failed`); + try { fs.rmSync(HOME, { recursive: true, force: true }); } catch { /* best-effort */ } + process.exit(fail ? 1 : 0); +})(); diff --git a/src/terminal.ts b/src/terminal.ts index 28b61386..aad6f83d 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -3066,6 +3066,14 @@ export class TerminalManager { // thread follow-ups are delivered by send-keys (see deliverToResident / reviveResident). if (o.resident) env.RESIDENT = '1'; env.AGENT_DIR = manifest.dir; + // Fence git discovery at the agents folder. An agent's folder usually isn't a repo, so git walks UP — + // and where the data home sits inside the product checkout it lands in agent-os itself. globex + // 2026-09-22: an engineer run did `cd ` then, unchained, + // `git remote set-url origin …client-app` + `git reset --hard origin/`. The cd failed, git + // resolved to the agent-os checkout, and the product's own source was replaced by another repo: + // `terminal/` gone, so every session launched afterwards crashed. Repos INSIDE the agent's folder + // (clones, worktrees, its own `git init`) are below the ceiling and unaffected. + try { env.GIT_CEILING_DIRECTORIES = fs.realpathSync(path.dirname(manifest.dir)); } catch { /* no dir → no walk to fence */ } env.HOOK = this.gateHookFor(runtime); // Tells the shared gate hook which tool→capability routing table to use. env.AOS_RUNTIME = runtime;