@saadnvd1/agent-os 0.3.664 → 0.3.671
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/app/api/audits/[kind]/[id]/route.test.ts +2 -2
- package/app/api/mcp-auth/callback/route.test.ts +42 -0
- package/app/api/mcp-auth/callback/route.ts +56 -0
- package/app/api/projects/init/route.test.ts +2 -4
- package/app/api/sessions/[id]/mcp-auth/route.test.ts +86 -0
- package/app/api/sessions/[id]/mcp-auth/route.ts +58 -0
- package/app/api/shares/route.test.ts +3 -4
- package/app/api/workspaces/[id]/route.ts +9 -1
- package/app/api/worktree-sweep/route.test.ts +21 -0
- package/app/api/worktree-sweep/route.ts +24 -4
- package/components/Chat/ChatPanel.tsx +6 -3
- package/components/Chat/McpServers.tsx +61 -24
- package/components/Chat/McpSignIn.tsx +122 -0
- package/components/Chat/session.ts +8 -0
- package/components/Health/SweepRuns.tsx +1 -0
- package/components/Workspaces/WorkspaceSettings.tsx +6 -0
- package/data/chat/mcpAuth.ts +42 -0
- package/data/workspaces/index.ts +6 -0
- package/lib/chat/driver.ts +24 -2
- package/lib/chat/drivers/claude-approvals.ts +7 -0
- package/lib/chat/drivers/claude-mcp-auth.ts +82 -0
- package/lib/chat/drivers/claude.ts +7 -0
- package/lib/chat/events.ts +12 -0
- package/lib/chat/mcp-auth.ts +193 -0
- package/lib/chat/registry.ts +2 -0
- package/lib/chat/runner.ts +18 -0
- package/lib/chat/worker/client.ts +35 -1
- package/lib/chat/worker/host.ts +9 -0
- package/lib/chat/worker/main.ts +1 -1
- package/lib/chat/worker/mcp-auth.ts +119 -0
- package/lib/chat/worker/protocol.ts +19 -0
- package/lib/db/migrations/20261018T1730_mcp_sign_ins.ts +22 -0
- package/lib/db/migrations/20261018T2100_orch_full_access.ts +16 -0
- package/lib/db/types.ts +3 -0
- package/lib/done/testing.ts +2 -2
- package/lib/env-setup.ts +8 -1
- package/lib/health/boots.ts +2 -8
- package/lib/orchestrator/armed.test-helpers.ts +3 -2
- package/lib/orchestrator/brief.ts +14 -9
- package/lib/orchestrator/external-gates.ts +62 -1
- package/lib/orchestrator/full-access.ts +21 -0
- package/lib/orchestrator/quick.ts +47 -4
- package/lib/orchestrator/serve.ts +11 -2
- package/lib/orchestrator/shell-guard.ts +23 -1
- package/lib/orchestrator/signoff.test-helpers.ts +3 -2
- package/lib/orchestrator/sweep-arm.ts +180 -0
- package/lib/orchestrator/tool-names.ts +4 -1
- package/lib/orchestrator/tool-schemas.ts +33 -1
- package/lib/orchestrator/tools.ts +6 -2
- package/lib/orchestrator/write-guard.ts +34 -7
- package/lib/project-config/brief.ts +3 -1
- package/lib/project-config/database.ts +6 -0
- package/lib/project-config/index.ts +108 -21
- package/lib/project-config/schema.ts +10 -4
- package/lib/project-config/session.ts +9 -6
- package/lib/security/net.ts +7 -4
- package/lib/tasks/move-testing.ts +2 -2
- package/lib/test-temp.ts +30 -0
- package/lib/workspaces.ts +6 -2
- package/lib/worktree-sweep/caches.ts +6 -0
- package/lib/worktree-sweep/extras.ts +16 -8
- package/lib/worktree-sweep/report.ts +1 -0
- package/lib/worktree-sweep/settings.ts +2 -2
- package/lib/worktree-sweep/testTemp.ts +113 -0
- package/lib/worktree-sweep/types.ts +2 -0
- package/package.json +1 -1
- package/scripts/autodeploy.test.ts +2 -2
- package/scripts/check-bundle.test.ts +2 -2
- package/scripts/hooks/banned-terms.test.ts +3 -2
- package/scripts/hooks/staged-secrets.test.ts +2 -2
- package/scripts/lib/build-swap.test.ts +2 -3
- package/scripts/lib/wsl.test.ts +2 -3
- package/scripts/update-run.test.ts +2 -2
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The one reader of a project's config: `agentos.json
|
|
3
|
-
* else the legacy `.agent-os/worktrees.json` / `.agent-os.json
|
|
4
|
-
*
|
|
5
|
-
* to read the next
|
|
2
|
+
* The one reader of a project's config: `agentos.json` and `.dispatch.json`
|
|
3
|
+
* together, else the legacy `.agent-os/worktrees.json` / `.agent-os.json`
|
|
4
|
+
* (the first of those that exists). A broken file is an error, never a
|
|
5
|
+
* reason to read the next or to ignore it.
|
|
6
|
+
*
|
|
7
|
+
* When both agentos.json and .dispatch.json exist they merge by top-level
|
|
8
|
+
* key: agentos.json wins any key it sets, and its value replaces
|
|
9
|
+
* .dispatch.json's whole (no deep merge, so a partial `browse` never mixes
|
|
10
|
+
* with the old one); keys only .dispatch.json has still apply, after its
|
|
11
|
+
* normalization (`workspace` becomes `cards.workspace`, dispatch-only keys
|
|
12
|
+
* are dropped). `inherited` names those keys and `warning` says to move them.
|
|
13
|
+
* Design: docs/decisions/2026-10-10-agentos-json.md.
|
|
6
14
|
*/
|
|
7
15
|
|
|
8
16
|
import * as fs from "fs";
|
|
9
17
|
import * as path from "path";
|
|
10
18
|
import type { z } from "zod";
|
|
11
19
|
import {
|
|
20
|
+
agentosFields,
|
|
12
21
|
agentosJson,
|
|
22
|
+
dispatchFields,
|
|
13
23
|
dispatchJson,
|
|
14
24
|
legacyJson,
|
|
25
|
+
mergedJson,
|
|
15
26
|
RESERVED_ENV,
|
|
16
27
|
type ProjectConfig,
|
|
17
28
|
} from "./schema";
|
|
@@ -34,9 +45,20 @@ export interface LoadedConfig {
|
|
|
34
45
|
source: string | null;
|
|
35
46
|
// Why the file was refused, naming it and every bad field.
|
|
36
47
|
error: string | null;
|
|
48
|
+
// Top-level keys that came from .dispatch.json because agentos.json lacks
|
|
49
|
+
// them; empty unless both files exist.
|
|
50
|
+
inherited: string[];
|
|
51
|
+
// One line for the brief when both files exist; null otherwise.
|
|
52
|
+
warning: string | null;
|
|
37
53
|
}
|
|
38
54
|
|
|
39
|
-
const NONE: LoadedConfig = {
|
|
55
|
+
export const NONE: LoadedConfig = Object.freeze({
|
|
56
|
+
config: {},
|
|
57
|
+
source: null,
|
|
58
|
+
error: null,
|
|
59
|
+
inherited: Object.freeze([]) as unknown as string[],
|
|
60
|
+
warning: null,
|
|
61
|
+
});
|
|
40
62
|
|
|
41
63
|
function describe(file: string, error: z.ZodError): string {
|
|
42
64
|
const fields = error.issues.map((i) => {
|
|
@@ -61,7 +83,9 @@ function fromLegacy(raw: z.infer<typeof legacyJson>): ProjectConfig {
|
|
|
61
83
|
return config;
|
|
62
84
|
}
|
|
63
85
|
|
|
64
|
-
|
|
86
|
+
// `checked: false` leaves the port-name check to the merge, which can see
|
|
87
|
+
// the ports the other file declares.
|
|
88
|
+
function parse(file: string, text: string, checked = true): LoadedConfig {
|
|
65
89
|
let raw: unknown;
|
|
66
90
|
try {
|
|
67
91
|
raw = JSON.parse(text);
|
|
@@ -73,31 +97,102 @@ function parse(file: string, text: string): LoadedConfig {
|
|
|
73
97
|
};
|
|
74
98
|
}
|
|
75
99
|
if (file === "agentos.json") {
|
|
76
|
-
const got = agentosJson.safeParse(raw);
|
|
100
|
+
const got = (checked ? agentosJson : agentosFields).safeParse(raw);
|
|
77
101
|
return got.success
|
|
78
|
-
? { config: got.data, source: file
|
|
102
|
+
? { ...NONE, config: got.data, source: file }
|
|
79
103
|
: { ...NONE, source: file, error: describe(file, got.error) };
|
|
80
104
|
}
|
|
81
105
|
if (file === ".dispatch.json") {
|
|
82
|
-
const got = dispatchJson.safeParse(raw);
|
|
106
|
+
const got = (checked ? dispatchJson : dispatchFields).safeParse(raw);
|
|
83
107
|
if (!got.success)
|
|
84
108
|
return { ...NONE, source: file, error: describe(file, got.error) };
|
|
85
109
|
const { workspace, ...config } = got.data;
|
|
86
110
|
if (workspace && !config.cards?.workspace)
|
|
87
111
|
config.cards = { ...config.cards, workspace };
|
|
88
|
-
return { config, source: file
|
|
112
|
+
return { ...NONE, config, source: file };
|
|
89
113
|
}
|
|
90
114
|
const got = legacyJson.safeParse(raw);
|
|
91
115
|
return got.success
|
|
92
|
-
? { config: fromLegacy(got.data), source: file
|
|
116
|
+
? { ...NONE, config: fromLegacy(got.data), source: file }
|
|
93
117
|
: { ...NONE, source: file, error: describe(file, got.error) };
|
|
94
118
|
}
|
|
95
119
|
|
|
120
|
+
export const bothFilesWarning = (inherited: string[]): string =>
|
|
121
|
+
`This repo has both agentos.json and .dispatch.json; keys ${inherited.join(", ")} come from .dispatch.json. Move them into agentos.json and delete .dispatch.json.`;
|
|
122
|
+
|
|
123
|
+
function merge(agentos: ProjectConfig, dispatch: ProjectConfig): LoadedConfig {
|
|
124
|
+
const inherited = Object.keys(dispatch).filter((k) => !(k in agentos));
|
|
125
|
+
const config = {
|
|
126
|
+
...Object.fromEntries(
|
|
127
|
+
inherited.map((k) => [k, dispatch[k as keyof ProjectConfig]])
|
|
128
|
+
),
|
|
129
|
+
...agentos,
|
|
130
|
+
} as ProjectConfig;
|
|
131
|
+
const got = mergedJson.safeParse(config);
|
|
132
|
+
if (!got.success)
|
|
133
|
+
return {
|
|
134
|
+
...NONE,
|
|
135
|
+
source: "agentos.json",
|
|
136
|
+
error: describe("agentos.json with .dispatch.json", got.error),
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
...NONE,
|
|
140
|
+
config,
|
|
141
|
+
source: "agentos.json",
|
|
142
|
+
inherited,
|
|
143
|
+
warning: inherited.length ? bothFilesWarning(inherited) : null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function statOf(projectPath: string, file: string): fs.Stats | null {
|
|
148
|
+
try {
|
|
149
|
+
const stat = fs.statSync(path.join(projectPath, file));
|
|
150
|
+
return stat.isFile() ? stat : null;
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readFile(projectPath: string, file: string): string | LoadedConfig {
|
|
157
|
+
try {
|
|
158
|
+
return fs.readFileSync(path.join(projectPath, file), "utf-8");
|
|
159
|
+
} catch (e) {
|
|
160
|
+
return {
|
|
161
|
+
...NONE,
|
|
162
|
+
source: file,
|
|
163
|
+
error: `${file} could not be read: ${(e as Error).message}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Both files, merged. A broken one is the error, whichever it is.
|
|
169
|
+
function loadBoth(projectPath: string): LoadedConfig {
|
|
170
|
+
const parsed: LoadedConfig[] = [];
|
|
171
|
+
for (const file of ["agentos.json", ".dispatch.json"]) {
|
|
172
|
+
const text = readFile(projectPath, file);
|
|
173
|
+
if (typeof text !== "string") return text;
|
|
174
|
+
const got = parse(file, text, false);
|
|
175
|
+
if (got.error) return got;
|
|
176
|
+
parsed.push(got);
|
|
177
|
+
}
|
|
178
|
+
return merge(parsed[0].config, parsed[1].config);
|
|
179
|
+
}
|
|
180
|
+
|
|
96
181
|
// Keyed on the project; reused while the file's mtime and size hold, since
|
|
97
182
|
// agentEnv reads it for every terminal and chat start.
|
|
98
183
|
const cache = new Map<string, { stamp: string; loaded: LoadedConfig }>();
|
|
99
184
|
|
|
100
185
|
export function loadProjectConfig(projectPath: string): LoadedConfig {
|
|
186
|
+
const a = statOf(projectPath, "agentos.json");
|
|
187
|
+
const d = statOf(projectPath, ".dispatch.json");
|
|
188
|
+
if (a && d) {
|
|
189
|
+
const stamp = `both:${a.mtimeMs}:${a.size}:${d.mtimeMs}:${d.size}`;
|
|
190
|
+
const hit = cache.get(projectPath);
|
|
191
|
+
if (hit?.stamp === stamp) return hit.loaded;
|
|
192
|
+
const loaded = loadBoth(projectPath);
|
|
193
|
+
cache.set(projectPath, { stamp, loaded });
|
|
194
|
+
return loaded;
|
|
195
|
+
}
|
|
101
196
|
for (const file of CONFIG_FILES) {
|
|
102
197
|
const full = path.join(projectPath, file);
|
|
103
198
|
let stat: fs.Stats;
|
|
@@ -110,16 +205,8 @@ export function loadProjectConfig(projectPath: string): LoadedConfig {
|
|
|
110
205
|
const stamp = `${file}:${stat.mtimeMs}:${stat.size}`;
|
|
111
206
|
const hit = cache.get(projectPath);
|
|
112
207
|
if (hit?.stamp === stamp) return hit.loaded;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
text = fs.readFileSync(full, "utf-8");
|
|
116
|
-
} catch (e) {
|
|
117
|
-
return {
|
|
118
|
-
...NONE,
|
|
119
|
-
source: file,
|
|
120
|
-
error: `${file} could not be read: ${(e as Error).message}`,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
208
|
+
const text = readFile(projectPath, file);
|
|
209
|
+
if (typeof text !== "string") return text;
|
|
123
210
|
const loaded = parse(file, text);
|
|
124
211
|
cache.set(projectPath, { stamp, loaded });
|
|
125
212
|
return loaded;
|
|
@@ -228,13 +228,19 @@ function portRefs(
|
|
|
228
228
|
|
|
229
229
|
// agentos.json: an unknown key is an error, because it is nearly always a
|
|
230
230
|
// typo that would otherwise be silently ignored.
|
|
231
|
-
export const
|
|
231
|
+
export const agentosFields = z.strictObject(shape);
|
|
232
|
+
export const agentosJson = agentosFields.superRefine(portRefs);
|
|
232
233
|
|
|
233
234
|
// .dispatch.json: the same fields, but dispatch has keys of its own. Its
|
|
234
235
|
// top-level `workspace` is `cards.workspace` here.
|
|
235
|
-
export const
|
|
236
|
-
|
|
237
|
-
.
|
|
236
|
+
export const dispatchFields = z.object({
|
|
237
|
+
...shape,
|
|
238
|
+
workspace: z.string().optional(),
|
|
239
|
+
});
|
|
240
|
+
export const dispatchJson = dispatchFields.superRefine(portRefs);
|
|
241
|
+
|
|
242
|
+
// The cross-key port check on its own, for a config merged from two files.
|
|
243
|
+
export const mergedJson = z.looseObject(shape).superRefine(portRefs);
|
|
238
244
|
|
|
239
245
|
export const legacyJson = z.object({
|
|
240
246
|
setup: z.array(z.string()).optional(),
|
|
@@ -10,7 +10,12 @@ import { sessionTmpDir } from "../sessions/leftovers";
|
|
|
10
10
|
import { db } from "../db";
|
|
11
11
|
import { getProject } from "../projects";
|
|
12
12
|
import { sessionPorts } from "../ports";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
loadProjectConfig,
|
|
15
|
+
NONE,
|
|
16
|
+
projectEnv,
|
|
17
|
+
type LoadedConfig,
|
|
18
|
+
} from "./index";
|
|
14
19
|
import { runningBrief } from "./brief";
|
|
15
20
|
import { databaseEnv, sessionDatabase } from "./database";
|
|
16
21
|
|
|
@@ -31,9 +36,7 @@ export function sessionConfig(
|
|
|
31
36
|
sessionId: string
|
|
32
37
|
): LoadedConfig & { ports: Record<string, number> | null } {
|
|
33
38
|
const projectPath = projectPathOf(sessionId);
|
|
34
|
-
const loaded = projectPath
|
|
35
|
-
? loadProjectConfig(projectPath)
|
|
36
|
-
: { config: {}, source: null, error: null };
|
|
39
|
+
const loaded = projectPath ? loadProjectConfig(projectPath) : NONE;
|
|
37
40
|
return { ...loaded, ports: sessionPorts(sessionId) };
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -70,9 +73,9 @@ export function sessionProjectEnv(sessionId: string): Record<string, string> {
|
|
|
70
73
|
|
|
71
74
|
export function sessionRunningBrief(sessionId: string): string {
|
|
72
75
|
try {
|
|
73
|
-
const { config, source, ports } = sessionConfig(sessionId);
|
|
76
|
+
const { config, source, warning, ports } = sessionConfig(sessionId);
|
|
74
77
|
return source
|
|
75
|
-
? runningBrief(config, ports ?? {}, sessionDatabase(sessionId))
|
|
78
|
+
? runningBrief(config, ports ?? {}, sessionDatabase(sessionId), warning)
|
|
76
79
|
: "";
|
|
77
80
|
} catch (error) {
|
|
78
81
|
console.error(`[project-config] brief for ${sessionId}:`, error);
|
package/lib/security/net.ts
CHANGED
|
@@ -146,10 +146,13 @@ export function originAllowed(
|
|
|
146
146
|
|
|
147
147
|
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
148
148
|
|
|
149
|
-
// LumifyHub's approve page
|
|
150
|
-
// reached by a cross-site top-level navigation.
|
|
151
|
-
// this instance issued, which a foreign page can't know.
|
|
152
|
-
const CROSS_SITE_NAVIGATIONS = [
|
|
149
|
+
// LumifyHub's approve page and an MCP server's sign-in send the browser back
|
|
150
|
+
// here, so these URLs are reached by a cross-site top-level navigation. Each
|
|
151
|
+
// acts only on a `state` this instance issued, which a foreign page can't know.
|
|
152
|
+
const CROSS_SITE_NAVIGATIONS = [
|
|
153
|
+
"/api/lumifyhub/callback",
|
|
154
|
+
"/api/mcp-auth/callback",
|
|
155
|
+
];
|
|
153
156
|
|
|
154
157
|
function crossSiteNavigationAllowed(req: {
|
|
155
158
|
method?: string;
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// Tests mock ../worktrees (WORKTREES_DIR), ../env-setup and ../agents/launch.
|
|
4
4
|
|
|
5
5
|
import fs from "fs";
|
|
6
|
-
import os from "os";
|
|
7
6
|
import path from "path";
|
|
8
7
|
import { randomUUID } from "crypto";
|
|
9
8
|
import { db, insertSessionRow, type Session } from "../db";
|
|
10
9
|
import { createProject } from "../projects";
|
|
11
10
|
import { writeTranscript } from "./transcript";
|
|
12
11
|
import { gitSync } from "../git-exec";
|
|
12
|
+
import { tempDir } from "@/lib/test-temp";
|
|
13
13
|
|
|
14
14
|
export const CLAUDE_ID = "11111111-2222-3333-4444-555555555555";
|
|
15
15
|
|
|
@@ -30,7 +30,7 @@ export interface MoveFixture {
|
|
|
30
30
|
/** HOME, Claude's folder and git identity all point into a temp dir. */
|
|
31
31
|
export function setupMoveRepo(): MoveFixture {
|
|
32
32
|
const saved = { HOME: process.env.HOME };
|
|
33
|
-
const tmp =
|
|
33
|
+
const tmp = tempDir("aos-move-");
|
|
34
34
|
process.env.HOME = tmp;
|
|
35
35
|
process.env.CLAUDE_CONFIG_DIR = path.join(tmp, ".claude");
|
|
36
36
|
for (const who of ["AUTHOR", "COMMITTER"]) {
|
package/lib/test-temp.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Folders made by tempDir, removed when the test file ends (vitest.setup.ts
|
|
6
|
+
// calls removeTempDirs). Kept on globalThis: vi.resetModules() would
|
|
7
|
+
// otherwise lose the list.
|
|
8
|
+
const KEY = Symbol.for("agentos.testTempDirs");
|
|
9
|
+
const made = (): Set<string> => {
|
|
10
|
+
const g = globalThis as unknown as Record<symbol, Set<string> | undefined>;
|
|
11
|
+
return (g[KEY] ??= new Set<string>());
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** A fresh temp folder that is removed when the test file ends. */
|
|
15
|
+
export function tempDir(prefix: string): string {
|
|
16
|
+
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
17
|
+
made().add(dir);
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function removeTempDirs(): void {
|
|
22
|
+
for (const dir of made()) {
|
|
23
|
+
try {
|
|
24
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 5 });
|
|
25
|
+
} catch {
|
|
26
|
+
// the run root's removal is the backstop
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
made().clear();
|
|
30
|
+
}
|
package/lib/workspaces.ts
CHANGED
|
@@ -8,10 +8,11 @@ import { db, type Workspace } from "./db";
|
|
|
8
8
|
|
|
9
9
|
type WorkspaceRow = Omit<
|
|
10
10
|
Workspace,
|
|
11
|
-
"collapsed" | "wake_tasks_on_pr" | "orch_mcp_servers"
|
|
11
|
+
"collapsed" | "wake_tasks_on_pr" | "orch_mcp_servers" | "orch_full_access"
|
|
12
12
|
> & {
|
|
13
13
|
collapsed: number;
|
|
14
14
|
wake_tasks_on_pr: number;
|
|
15
|
+
orch_full_access: number;
|
|
15
16
|
orch_mcp_servers: string;
|
|
16
17
|
};
|
|
17
18
|
|
|
@@ -38,6 +39,7 @@ const toWorkspace = (row: WorkspaceRow): Workspace => ({
|
|
|
38
39
|
...row,
|
|
39
40
|
collapsed: !!row.collapsed,
|
|
40
41
|
wake_tasks_on_pr: !!row.wake_tasks_on_pr,
|
|
42
|
+
orch_full_access: !!row.orch_full_access,
|
|
41
43
|
orch_mcp_servers: parseServers(row.orch_mcp_servers),
|
|
42
44
|
});
|
|
43
45
|
|
|
@@ -86,6 +88,7 @@ export function updateWorkspace(
|
|
|
86
88
|
sortOrder?: number;
|
|
87
89
|
maxRunningTasks?: number | null;
|
|
88
90
|
wakeTasksOnPR?: boolean;
|
|
91
|
+
orchFullAccess?: boolean;
|
|
89
92
|
mcpServers?: string[];
|
|
90
93
|
}
|
|
91
94
|
): Workspace | null {
|
|
@@ -93,13 +96,14 @@ export function updateWorkspace(
|
|
|
93
96
|
if (!existing) return null;
|
|
94
97
|
const name = updates.name?.trim() || existing.name;
|
|
95
98
|
db.prepare(
|
|
96
|
-
`UPDATE workspaces SET name = ?, collapsed = ?, sort_order = ?, max_running_tasks = ?, wake_tasks_on_pr = ?, orch_mcp_servers = ? WHERE id = ?`
|
|
99
|
+
`UPDATE workspaces SET name = ?, collapsed = ?, sort_order = ?, max_running_tasks = ?, wake_tasks_on_pr = ?, orch_full_access = ?, orch_mcp_servers = ? WHERE id = ?`
|
|
97
100
|
).run(
|
|
98
101
|
name,
|
|
99
102
|
(updates.collapsed ?? existing.collapsed) ? 1 : 0,
|
|
100
103
|
updates.sortOrder ?? existing.sort_order,
|
|
101
104
|
taskLimit(updates.maxRunningTasks, existing.max_running_tasks),
|
|
102
105
|
(updates.wakeTasksOnPR ?? existing.wake_tasks_on_pr) ? 1 : 0,
|
|
106
|
+
(updates.orchFullAccess ?? existing.orch_full_access) ? 1 : 0,
|
|
103
107
|
JSON.stringify(serverNames(updates.mcpServers, existing.orch_mcp_servers)),
|
|
104
108
|
id
|
|
105
109
|
);
|
|
@@ -10,6 +10,7 @@ import fs from "fs";
|
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { execFile } from "child_process";
|
|
12
12
|
import { SESSION_TMP_PREFIX, tmpName } from "../sessions/leftovers";
|
|
13
|
+
import { isTestTempName } from "./testTemp";
|
|
13
14
|
import type { Candidate, SweepSkipped } from "./types";
|
|
14
15
|
|
|
15
16
|
const DAY_MS = 24 * 3600_000;
|
|
@@ -92,6 +93,11 @@ export async function findCacheLeftovers(deps: CacheDeps): Promise<{
|
|
|
92
93
|
const livePrefixes = [...deps.liveIds].map(tmpName);
|
|
93
94
|
for (const dir of subdirs(deps.tmpRoot)) {
|
|
94
95
|
const name = path.basename(dir);
|
|
96
|
+
if (isTestTempName(name)) {
|
|
97
|
+
if (deps.now - mtime(dir) >= DAY_MS)
|
|
98
|
+
await add(dir, "test-temp", "left behind by a test run");
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
95
101
|
const isSession = name.startsWith(SESSION_TMP_PREFIX);
|
|
96
102
|
if (!isSession && !name.startsWith("agentos-preview-")) continue;
|
|
97
103
|
if (deps.now - mtime(dir) < DAY_MS) continue;
|
|
@@ -69,6 +69,18 @@ export async function cachedRootSize(
|
|
|
69
69
|
|
|
70
70
|
export const forgetRootSizes = () => rootSizes.clear();
|
|
71
71
|
|
|
72
|
+
// Removes a folder directly inside one of the roots, and nothing else.
|
|
73
|
+
export async function dropFolderIn(
|
|
74
|
+
roots: string[],
|
|
75
|
+
folder: string
|
|
76
|
+
): Promise<boolean> {
|
|
77
|
+
const target = path.resolve(folder);
|
|
78
|
+
if (!roots.some((r) => r && path.dirname(target) === path.resolve(r)))
|
|
79
|
+
return false;
|
|
80
|
+
await fs.promises.rm(target, { recursive: true, force: true });
|
|
81
|
+
return !fs.existsSync(target);
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
export async function defaultExtras(
|
|
73
85
|
opts: { fresh?: boolean } = {}
|
|
74
86
|
): Promise<Extras> {
|
|
@@ -108,14 +120,10 @@ export async function defaultExtras(
|
|
|
108
120
|
count: dirCount(SPARE_ROOT),
|
|
109
121
|
bytes: await cachedRootSize(SPARE_ROOT, size, !!opts.fresh),
|
|
110
122
|
},
|
|
111
|
-
drop: async (c) =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return false;
|
|
116
|
-
await fs.promises.rm(target, { recursive: true, force: true });
|
|
117
|
-
return !fs.existsSync(target);
|
|
118
|
-
},
|
|
123
|
+
drop: async (c) =>
|
|
124
|
+
c.kind === "database"
|
|
125
|
+
? dropTaskDatabase(c, froms)
|
|
126
|
+
: dropFolderIn(roots, c.path),
|
|
119
127
|
};
|
|
120
128
|
}
|
|
121
129
|
|
|
@@ -83,8 +83,8 @@ export function projectSweepOverrides(): Array<{
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// The full clean-up (test leftovers, task databases, spares, temp folders,
|
|
86
|
-
// build caches) removes nothing until
|
|
87
|
-
//
|
|
86
|
+
// build caches) removes nothing until Saad turns it on for this machine, or
|
|
87
|
+
// the orchestrator does on his word after a dry run from the last hour.
|
|
88
88
|
const ARMED_KEY = "worktree_sweep.reaper";
|
|
89
89
|
|
|
90
90
|
export function reaperArmed(): boolean {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Folders the test suite makes in the OS temp folder, by name. The sweep
|
|
2
|
+
// takes only these, never another aos-… or agent-os-… folder, so AgentOS's
|
|
3
|
+
// own temp folders (aos-review-, aos-rebase-…) and a person's are safe.
|
|
4
|
+
// lib/test-temp.test.ts fails when a test uses a prefix missing here.
|
|
5
|
+
export const TEST_TEMP_PREFIXES: readonly string[] = [
|
|
6
|
+
"agent-os-git-",
|
|
7
|
+
"agent-os-home-",
|
|
8
|
+
"agent-os-kept-",
|
|
9
|
+
"agent-os-limits-",
|
|
10
|
+
"agent-os-session-logs-",
|
|
11
|
+
"agent-os-test-",
|
|
12
|
+
"aos-acc-",
|
|
13
|
+
"aos-add-",
|
|
14
|
+
"aos-armed-repo-",
|
|
15
|
+
"aos-art-",
|
|
16
|
+
"aos-autodeploy-",
|
|
17
|
+
"aos-caches-",
|
|
18
|
+
"aos-channel-",
|
|
19
|
+
"aos-clone-",
|
|
20
|
+
"aos-config-",
|
|
21
|
+
"aos-crypt-",
|
|
22
|
+
"aos-db-",
|
|
23
|
+
"aos-db-drops-",
|
|
24
|
+
"aos-db-wt-",
|
|
25
|
+
"aos-decl-",
|
|
26
|
+
"aos-device-keys-",
|
|
27
|
+
"aos-done-home-",
|
|
28
|
+
"aos-done-repo-",
|
|
29
|
+
"aos-env-",
|
|
30
|
+
"aos-fast-",
|
|
31
|
+
"aos-files-",
|
|
32
|
+
"aos-git-",
|
|
33
|
+
"aos-gr-",
|
|
34
|
+
"aos-heavy-",
|
|
35
|
+
"aos-init-route-",
|
|
36
|
+
"aos-install-",
|
|
37
|
+
"aos-kind-",
|
|
38
|
+
"aos-launch-plain-",
|
|
39
|
+
"aos-launch-repo-",
|
|
40
|
+
"aos-limits-",
|
|
41
|
+
"aos-mcp-",
|
|
42
|
+
"aos-merge-policy-",
|
|
43
|
+
"aos-mig-",
|
|
44
|
+
"aos-move-",
|
|
45
|
+
"aos-no-approvals-",
|
|
46
|
+
"aos-nogit-",
|
|
47
|
+
"aos-notify-",
|
|
48
|
+
"aos-orch-act-",
|
|
49
|
+
"aos-orch-add-",
|
|
50
|
+
"aos-orch-approve-",
|
|
51
|
+
"aos-orch-armed-",
|
|
52
|
+
"aos-orch-asks-",
|
|
53
|
+
"aos-orch-brakes-",
|
|
54
|
+
"aos-orch-ext-repo-",
|
|
55
|
+
"aos-orch-external-",
|
|
56
|
+
"aos-orch-home-",
|
|
57
|
+
"aos-orch-move-",
|
|
58
|
+
"aos-orch-plain-",
|
|
59
|
+
"aos-orch-questions-",
|
|
60
|
+
"aos-orch-recipes-",
|
|
61
|
+
"aos-orch-remind-",
|
|
62
|
+
"aos-orch-repo-",
|
|
63
|
+
"aos-orch-rules-",
|
|
64
|
+
"aos-orch-settle-",
|
|
65
|
+
"aos-orch-signoff-",
|
|
66
|
+
"aos-orch-sweep-",
|
|
67
|
+
"aos-origin-",
|
|
68
|
+
"aos-path-",
|
|
69
|
+
"aos-plain-",
|
|
70
|
+
"aos-plan-",
|
|
71
|
+
"aos-ports-",
|
|
72
|
+
"aos-pr-",
|
|
73
|
+
"aos-pr-async-",
|
|
74
|
+
"aos-pr-create-",
|
|
75
|
+
"aos-pr-gen-",
|
|
76
|
+
"aos-pr-route-",
|
|
77
|
+
"aos-prompts-",
|
|
78
|
+
"aos-quick-",
|
|
79
|
+
"aos-quick-repo-",
|
|
80
|
+
"aos-relay-",
|
|
81
|
+
"aos-relay-none-",
|
|
82
|
+
"aos-report-",
|
|
83
|
+
"aos-resurrect-claude-",
|
|
84
|
+
"aos-resurrect-home-",
|
|
85
|
+
"aos-run-",
|
|
86
|
+
"aos-runner-",
|
|
87
|
+
"aos-sec-",
|
|
88
|
+
"aos-shot-",
|
|
89
|
+
"aos-sim-",
|
|
90
|
+
"aos-spare-",
|
|
91
|
+
"aos-stack-",
|
|
92
|
+
"aos-start-opts-",
|
|
93
|
+
"aos-start-plain-",
|
|
94
|
+
"aos-sweep-own-",
|
|
95
|
+
"aos-t-",
|
|
96
|
+
"aos-task-diff-",
|
|
97
|
+
"aos-temp-helper-",
|
|
98
|
+
"aos-tmux-",
|
|
99
|
+
"aos-transcripts-",
|
|
100
|
+
"aos-ui-",
|
|
101
|
+
"aos-upd-",
|
|
102
|
+
"aos-usage-",
|
|
103
|
+
"aos-wg-",
|
|
104
|
+
"aos-wg-path-",
|
|
105
|
+
"aos-wl-",
|
|
106
|
+
"aos-worker-env-",
|
|
107
|
+
"aos-worker-tmux-",
|
|
108
|
+
"aos-wsl-",
|
|
109
|
+
"aos-wt-",
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
export const isTestTempName = (name: string) =>
|
|
113
|
+
TEST_TEMP_PREFIXES.some((p) => name.startsWith(p));
|
|
@@ -6,6 +6,7 @@ export type SweepKind =
|
|
|
6
6
|
| "database"
|
|
7
7
|
| "spare"
|
|
8
8
|
| "temp"
|
|
9
|
+
| "test-temp"
|
|
9
10
|
| "derived-data";
|
|
10
11
|
|
|
11
12
|
// Kinds that go only once the full clean-up is turned on.
|
|
@@ -14,6 +15,7 @@ export const ARMED_KINDS: ReadonlySet<SweepKind> = new Set([
|
|
|
14
15
|
"database",
|
|
15
16
|
"spare",
|
|
16
17
|
"temp",
|
|
18
|
+
"test-temp",
|
|
17
19
|
"derived-data",
|
|
18
20
|
]);
|
|
19
21
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from "child_process";
|
|
2
2
|
import * as fs from "fs";
|
|
3
|
-
import * as os from "os";
|
|
4
3
|
import * as path from "path";
|
|
5
4
|
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { tempDir } from "@/lib/test-temp";
|
|
6
6
|
|
|
7
7
|
// Runs the real scripts/autodeploy in a scratch clone with a stub redeploy
|
|
8
8
|
// (a pull) and wait-quiet, against a scratch origin and HOME.
|
|
@@ -11,7 +11,7 @@ const sh = (cwd: string, ...args: string[]) =>
|
|
|
11
11
|
execFileSync("git", args, { cwd, encoding: "utf-8" }).trim();
|
|
12
12
|
|
|
13
13
|
function setup() {
|
|
14
|
-
const root =
|
|
14
|
+
const root = tempDir("aos-autodeploy-");
|
|
15
15
|
const origin = path.join(root, "origin.git");
|
|
16
16
|
const work = path.join(root, "work");
|
|
17
17
|
const clone = path.join(root, "live");
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { spawnSync } from "child_process";
|
|
3
3
|
import fs from "fs";
|
|
4
|
-
import os from "os";
|
|
5
4
|
import path from "path";
|
|
5
|
+
import { tempDir } from "@/lib/test-temp";
|
|
6
6
|
|
|
7
7
|
const script = path.join(__dirname, "check-bundle.mjs");
|
|
8
8
|
|
|
9
9
|
function run(stats: unknown) {
|
|
10
|
-
const dir =
|
|
10
|
+
const dir = tempDir("bundle-");
|
|
11
11
|
const file = path.join(dir, "stats.json");
|
|
12
12
|
fs.writeFileSync(file, JSON.stringify(stats));
|
|
13
13
|
const r = spawnSync(process.execPath, [script, file], { encoding: "utf-8" });
|
|
@@ -4,11 +4,12 @@ import fs from "fs";
|
|
|
4
4
|
import os from "os";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { GENERIC_TERMS, loadTerms, matcher, scanTree } from "./banned-terms";
|
|
7
|
+
import { tempDir } from "@/lib/test-temp";
|
|
7
8
|
|
|
8
9
|
const generic = GENERIC_TERMS[0];
|
|
9
10
|
|
|
10
11
|
function tempRepo() {
|
|
11
|
-
const repo =
|
|
12
|
+
const repo = tempDir("banned-terms-");
|
|
12
13
|
const git = (...args: string[]) =>
|
|
13
14
|
execFileSync("git", args, { cwd: repo, stdio: "pipe" });
|
|
14
15
|
git("init", "-q");
|
|
@@ -22,7 +23,7 @@ describe("loadTerms", () => {
|
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
it("adds the private regexes, skipping blanks and comments", () => {
|
|
25
|
-
const dir =
|
|
26
|
+
const dir = tempDir("banned-names-");
|
|
26
27
|
const file = path.join(dir, "banned-names");
|
|
27
28
|
fs.writeFileSync(file, "# names\nacme[ -]?widgets\n\n zorp \n");
|
|
28
29
|
try {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { execFileSync, spawnSync } from "child_process";
|
|
3
3
|
import fs from "fs";
|
|
4
|
-
import os from "os";
|
|
5
4
|
import path from "path";
|
|
6
5
|
import { scanDiff } from "./staged-secrets";
|
|
6
|
+
import { tempDir } from "@/lib/test-temp";
|
|
7
7
|
|
|
8
8
|
// Fakes are built here, never written whole: a literal would be the very
|
|
9
9
|
// thing this scan (and the merge gate) refuses.
|
|
@@ -62,7 +62,7 @@ describe("scanDiff", () => {
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
it("fails the commit on a staged secret, and passes once it's gone", () => {
|
|
65
|
-
const repo =
|
|
65
|
+
const repo = tempDir("staged-secrets-");
|
|
66
66
|
const git = (...args: string[]) =>
|
|
67
67
|
execFileSync("git", args, { cwd: repo, stdio: "pipe" });
|
|
68
68
|
const scan = () =>
|