shadok-ai 0.17.3 → 0.17.4
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/dist/selfrepo.d.ts +11 -3
- package/dist/selfrepo.js +13 -5
- package/dist/selfrepo.js.map +1 -1
- package/dist/server.js +91 -22
- package/dist/server.js.map +1 -1
- package/dist/worktree.d.ts +19 -4
- package/dist/worktree.js +25 -10
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
package/dist/selfrepo.d.ts
CHANGED
|
@@ -6,8 +6,16 @@
|
|
|
6
6
|
* diff before connecting any GitHub account.
|
|
7
7
|
*/
|
|
8
8
|
export declare const SELF_REPO_URL = "https://github.com/shadok-ai/shadok-ai.git";
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Our own copy, never the directory the running server was launched from.
|
|
11
|
+
* `stateHome` defaults to the server's own home (mono-tenant / commons before
|
|
12
|
+
* migration), but is the DROPPED agent's home (`/home/<user>`) once agents run
|
|
13
|
+
* as a non-root OS user: the server's `/root` is mode 0700 and un-traversable by
|
|
14
|
+
* that user, so a clone there strands the Tweak agent — same reason as
|
|
15
|
+
* `worktreeDir` in `worktree.ts`. The caller makes the clone writable by the
|
|
16
|
+
* drop user; this only fixes WHERE it lands.
|
|
17
|
+
*/
|
|
18
|
+
export declare function selfRepoDir(stateHome?: string): string;
|
|
11
19
|
export type SelfRepoPlan = "clone" | "update" | "reclone";
|
|
12
20
|
/** Pure: what to do with the checkout, given what is on disk. */
|
|
13
21
|
export declare function selfRepoPlan(exists: boolean, isRepo: boolean): SelfRepoPlan;
|
|
@@ -26,4 +34,4 @@ export interface SelfRepoResult {
|
|
|
26
34
|
* worktrees a tweak session owns are separate checkouts and are never touched —
|
|
27
35
|
* refreshing under a live agent is how you lose its work.
|
|
28
36
|
*/
|
|
29
|
-
export declare function ensureSelfRepo(): SelfRepoResult;
|
|
37
|
+
export declare function ensureSelfRepo(stateHome?: string): SelfRepoResult;
|
package/dist/selfrepo.js
CHANGED
|
@@ -10,9 +10,17 @@ import path from "node:path";
|
|
|
10
10
|
* diff before connecting any GitHub account.
|
|
11
11
|
*/
|
|
12
12
|
export const SELF_REPO_URL = "https://github.com/shadok-ai/shadok-ai.git";
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Our own copy, never the directory the running server was launched from.
|
|
15
|
+
* `stateHome` defaults to the server's own home (mono-tenant / commons before
|
|
16
|
+
* migration), but is the DROPPED agent's home (`/home/<user>`) once agents run
|
|
17
|
+
* as a non-root OS user: the server's `/root` is mode 0700 and un-traversable by
|
|
18
|
+
* that user, so a clone there strands the Tweak agent — same reason as
|
|
19
|
+
* `worktreeDir` in `worktree.ts`. The caller makes the clone writable by the
|
|
20
|
+
* drop user; this only fixes WHERE it lands.
|
|
21
|
+
*/
|
|
22
|
+
export function selfRepoDir(stateHome = os.homedir()) {
|
|
23
|
+
return path.join(stateHome, ".shadok-ai", "self", "shadok-ai");
|
|
16
24
|
}
|
|
17
25
|
/** Pure: what to do with the checkout, given what is on disk. */
|
|
18
26
|
export function selfRepoPlan(exists, isRepo) {
|
|
@@ -48,8 +56,8 @@ function git(args, cwd) {
|
|
|
48
56
|
* worktrees a tweak session owns are separate checkouts and are never touched —
|
|
49
57
|
* refreshing under a live agent is how you lose its work.
|
|
50
58
|
*/
|
|
51
|
-
export function ensureSelfRepo() {
|
|
52
|
-
const dir = selfRepoDir();
|
|
59
|
+
export function ensureSelfRepo(stateHome = os.homedir()) {
|
|
60
|
+
const dir = selfRepoDir(stateHome);
|
|
53
61
|
const plan = selfRepoPlan(fs.existsSync(dir), fs.existsSync(path.join(dir, ".git")));
|
|
54
62
|
try {
|
|
55
63
|
if (plan === "reclone")
|
package/dist/selfrepo.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selfrepo.js","sourceRoot":"","sources":["../src/selfrepo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,4CAA4C,CAAC;AAE1E
|
|
1
|
+
{"version":3,"file":"selfrepo.js","sourceRoot":"","sources":["../src/selfrepo.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
CHANGED
|
@@ -1554,7 +1554,9 @@ app.get("/diff", (req, res) => {
|
|
|
1554
1554
|
// Past worktree sessions of a repo (for reopening unfinished work).
|
|
1555
1555
|
app.get("/recover", (req, res) => {
|
|
1556
1556
|
const repo = String(req.query.repo ?? "").trim() || process.cwd();
|
|
1557
|
-
const list = isGitRepo(repo)
|
|
1557
|
+
const list = isGitRepo(repo)
|
|
1558
|
+
? listPastSessions(repo, agentStateHome(commonsOsUser({ migrated: groupsMigrated, mode: DEPLOYMENT_MODE })))
|
|
1559
|
+
: [];
|
|
1558
1560
|
// `repo` is a caller-supplied path: a caller who knows another group's HOME
|
|
1559
1561
|
// could enumerate that group's past worktree sessions (branch/commit metadata +
|
|
1560
1562
|
// first-prompt preview). Filter each past session to the caller's visible
|
|
@@ -1617,10 +1619,18 @@ app.put("/mute", (req, res) => {
|
|
|
1617
1619
|
// Materialises shadok-ai's own source for the "Tweak Shadok-AI" CTA, and hands
|
|
1618
1620
|
// back the directory to start the agent in. Sits here so it inherits the same
|
|
1619
1621
|
// password gate as its neighbours.
|
|
1620
|
-
app.post("/tweak/prepare", (_req, res) => {
|
|
1621
|
-
|
|
1622
|
+
app.post("/tweak/prepare", async (_req, res) => {
|
|
1623
|
+
// The tweak agent runs as the commons drop user; its self-repo clone must live
|
|
1624
|
+
// in THAT user's home (reachable under 0700 /root) and be owned by it — same
|
|
1625
|
+
// reason as the worktree created off it. `commonsOsUser → null` in mono-tenant
|
|
1626
|
+
// makes `agentStateHome(null)` the server home and the chown a no-op, so
|
|
1627
|
+
// nothing changes there.
|
|
1628
|
+
const commonsUser = commonsOsUser({ migrated: groupsMigrated, mode: DEPLOYMENT_MODE });
|
|
1629
|
+
const r = ensureSelfRepo(agentStateHome(commonsUser));
|
|
1622
1630
|
if (r.error)
|
|
1623
1631
|
return res.status(500).json({ error: r.error });
|
|
1632
|
+
if (r.cwd)
|
|
1633
|
+
await chownAgentStateToDropUser(r.cwd, commonsUser);
|
|
1624
1634
|
res.json({ cwd: r.cwd });
|
|
1625
1635
|
});
|
|
1626
1636
|
// Running version + latest seen on npm (null if not yet polled / check disabled).
|
|
@@ -2120,11 +2130,16 @@ app.post("/groups/members", (req, res) => {
|
|
|
2120
2130
|
// DEPLOYMENT_MODE gate and the honest-failure rules (T3/T4) all live here. ──────
|
|
2121
2131
|
/** Where a group's HOME is created (`SHADOK_HOME_BASE` override, else `/home`). */
|
|
2122
2132
|
const GROUP_HOME_BASE = (process.env.SHADOK_HOME_BASE ?? "").trim() || DEFAULT_HOME_BASE;
|
|
2123
|
-
/** The HOME
|
|
2124
|
-
*
|
|
2125
|
-
*
|
|
2126
|
-
*
|
|
2127
|
-
|
|
2133
|
+
/** The HOME that holds ALL of an agent's on-disk state: its transcript
|
|
2134
|
+
* (`.claude/projects`, read by the content tail and `loadHistory`) AND its
|
|
2135
|
+
* worktrees + self-repo clone (`.shadok-ai/worktrees`, `.shadok-ai/self`). The
|
|
2136
|
+
* drop user's own home when the agent is privilege-dropped (multi-tenant
|
|
2137
|
+
* isolation), else the server's own home (commons / mono-tenant). The single
|
|
2138
|
+
* answer every one of those paths must use: the server's `/root` is mode 0700
|
|
2139
|
+
* and un-traversable by a dropped user, so state created there is silently
|
|
2140
|
+
* unreachable — a reply that never streams, or a Tweak agent wedged on Claude
|
|
2141
|
+
* Code's invalid-settings screen (its worktree lived under `/root`). */
|
|
2142
|
+
function agentStateHome(user) {
|
|
2128
2143
|
return user ? homeForUser(user, GROUP_HOME_BASE) : os.homedir();
|
|
2129
2144
|
}
|
|
2130
2145
|
/** The COMMONS tier for secret resolution: Main plus every PUBLIC non-Main group.
|
|
@@ -2138,11 +2153,13 @@ function commonsGroupIds() {
|
|
|
2138
2153
|
ids.push(g.id);
|
|
2139
2154
|
return ids;
|
|
2140
2155
|
}
|
|
2141
|
-
/** The
|
|
2142
|
-
* a
|
|
2143
|
-
* re-derived from the channel's
|
|
2144
|
-
* the
|
|
2145
|
-
|
|
2156
|
+
/** The state HOME for a CHANNEL that may not be live (e.g. GET /download of a
|
|
2157
|
+
* past session, or pruning a worktree as the session ends): a live session's
|
|
2158
|
+
* own `transcriptHome` when we have it, else re-derived from the channel's
|
|
2159
|
+
* group the same way a spawn's drop user is — the group's `sk-*` user, or the
|
|
2160
|
+
* commons user when migrated, else the server. Governs both the transcript and
|
|
2161
|
+
* the worktree location, so prune finds exactly what the spawn created. */
|
|
2162
|
+
function channelStateHome(chan) {
|
|
2146
2163
|
const live = sessions.get(chan.sessionId);
|
|
2147
2164
|
if (live)
|
|
2148
2165
|
return live.transcriptHome;
|
|
@@ -2150,7 +2167,7 @@ function channelTranscriptHome(chan) {
|
|
|
2150
2167
|
return os.homedir();
|
|
2151
2168
|
const g = chan.group ? loadGroups().find((x) => x.id === chan.group) : null;
|
|
2152
2169
|
const user = (g ? osUserFor(g) : null) ?? commonsOsUser({ migrated: groupsMigrated, mode: DEPLOYMENT_MODE });
|
|
2153
|
-
return
|
|
2170
|
+
return agentStateHome(user);
|
|
2154
2171
|
}
|
|
2155
2172
|
/** Task 12: the one shared pot every group HOME links as `~/shared`. */
|
|
2156
2173
|
const SHARED_POT_PATH = (process.env.SHADOK_SHARED_POT_PATH ?? "").trim() || DEFAULT_SHARED_POT_PATH;
|
|
@@ -2171,6 +2188,43 @@ function runPrivileged(cmd, argv) {
|
|
|
2171
2188
|
p.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${code}`))));
|
|
2172
2189
|
});
|
|
2173
2190
|
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Make a freshly-created worktree (or self-repo clone) usable by the DROP USER.
|
|
2193
|
+
* The server creates it as ROOT, so the checkout AND git's own worktree admin
|
|
2194
|
+
* dir (`<repo>/.git/worktrees/<name>`, where the agent's index/HEAD/logs live)
|
|
2195
|
+
* are root-owned — and a dropped agent could neither write them nor, before this
|
|
2196
|
+
* fix, even reach them under the 0700 server HOME. Now the checkout lives in the
|
|
2197
|
+
* drop user's OWN home (`agentStateHome` → `worktreeDir`), so only ownership is
|
|
2198
|
+
* left: chown BOTH the checkout and (for a worktree) its external admin dir to
|
|
2199
|
+
* the user. Best-effort and logged — a chown failure degrades to the old symptom
|
|
2200
|
+
* rather than failing the spawn, and the log names it. No-op with no drop user
|
|
2201
|
+
* (mono-tenant, or commons-as-root before migration): ownership is already right.
|
|
2202
|
+
*/
|
|
2203
|
+
async function chownAgentStateToDropUser(checkout, user) {
|
|
2204
|
+
if (!user)
|
|
2205
|
+
return;
|
|
2206
|
+
const owner = `${user}:${user}`;
|
|
2207
|
+
try {
|
|
2208
|
+
await runPrivileged("chown", ["-R", owner, checkout]);
|
|
2209
|
+
// git's admin dir sits under the REPO's gitdir, not the checkout; resolve it
|
|
2210
|
+
// from inside the checkout so a worktree `.git` FILE is followed to it. Only
|
|
2211
|
+
// chown it when OUTSIDE the checkout (a worktree); for a plain clone it is
|
|
2212
|
+
// `<checkout>/.git`, already covered by the recursive chown above.
|
|
2213
|
+
let adminDir = "";
|
|
2214
|
+
try {
|
|
2215
|
+
adminDir = execFileSync("git", ["-C", checkout, "rev-parse", "--absolute-git-dir"], { encoding: "utf8", timeout: 10_000 }).trim();
|
|
2216
|
+
}
|
|
2217
|
+
catch {
|
|
2218
|
+
/* not a git checkout: the recursive chown already covered it */
|
|
2219
|
+
}
|
|
2220
|
+
if (adminDir && !adminDir.startsWith(path.resolve(checkout) + path.sep)) {
|
|
2221
|
+
await runPrivileged("chown", ["-R", owner, adminDir]);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
catch (e) {
|
|
2225
|
+
console.warn(`state chown to ${user} failed for ${checkout} (agent may be unable to write it): ${e?.message ?? e}`);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2174
2228
|
/** The shadok skills to install: [package source dir, installed name]. Mirrored
|
|
2175
2229
|
* into the SERVER HOME by `seedXxxSkill` and, for multi-user isolation, into the
|
|
2176
2230
|
* machine-wide ENTERPRISE tier by `seedSystemSkills`. */
|
|
@@ -4125,7 +4179,7 @@ app.get("/download", (req, res) => {
|
|
|
4125
4179
|
if (!allowed) {
|
|
4126
4180
|
let raw;
|
|
4127
4181
|
try {
|
|
4128
|
-
raw = fs.readFileSync(transcriptFilePath(chan.cwd, session,
|
|
4182
|
+
raw = fs.readFileSync(transcriptFilePath(chan.cwd, session, channelStateHome(chan)), "utf8");
|
|
4129
4183
|
}
|
|
4130
4184
|
catch {
|
|
4131
4185
|
return res.status(403).type("text").send("that file was not sent by this agent");
|
|
@@ -5162,6 +5216,12 @@ async function endChannel(sessionId) {
|
|
|
5162
5216
|
const s = sessions.get(sessionId);
|
|
5163
5217
|
const worktreeBranch = s?.worktree?.branch ?? ch?.branch;
|
|
5164
5218
|
const repo = s?.worktree?.repo ?? ch?.repo;
|
|
5219
|
+
// Resolve the worktree's home WHILE the session is still live (its own
|
|
5220
|
+
// `transcriptHome` is the exact home the spawn created it in); fall back to the
|
|
5221
|
+
// channel's group-derived home once it is gone. Prune must look in the same
|
|
5222
|
+
// place createWorktree wrote, or a dropped agent's checkout under /home/<user>
|
|
5223
|
+
// would never be cleaned.
|
|
5224
|
+
const worktreeHome = s?.transcriptHome ?? (ch ? channelStateHome(ch) : os.homedir());
|
|
5165
5225
|
if (s) {
|
|
5166
5226
|
broadcast(s, { type: "stopped" });
|
|
5167
5227
|
await s.pilot.stop().catch(() => { });
|
|
@@ -5171,7 +5231,7 @@ async function endChannel(sessionId) {
|
|
|
5171
5231
|
// merged), keep it if there's uncommitted or unmerged work. Stops the empty-
|
|
5172
5232
|
// worktree pile-up while never discarding real work.
|
|
5173
5233
|
if (repo && worktreeBranch) {
|
|
5174
|
-
const outcome = pruneWorktree(repo, worktreeBranch);
|
|
5234
|
+
const outcome = pruneWorktree(repo, worktreeBranch, worktreeHome);
|
|
5175
5235
|
if (outcome !== "none")
|
|
5176
5236
|
console.log(`worktree ${worktreeBranch}: ${outcome} on session end`);
|
|
5177
5237
|
}
|
|
@@ -5270,8 +5330,8 @@ group = null) {
|
|
|
5270
5330
|
const pilot = makePilot(id, cwd, args, profile, model, effort, user, group);
|
|
5271
5331
|
// A dropped group agent writes its transcript under ITS OWN home, not the
|
|
5272
5332
|
// server's — so the content tail / history must read from there (invariant:
|
|
5273
|
-
// all content comes from the .jsonl). `
|
|
5274
|
-
const transcriptHome =
|
|
5333
|
+
// all content comes from the .jsonl). `agentStateHome` is the one answer.
|
|
5334
|
+
const transcriptHome = agentStateHome(user);
|
|
5275
5335
|
// Resumed sessions start with what the transcript already consumed.
|
|
5276
5336
|
const seededUsage = scanUsage(sessionFilePath(cwd, id, transcriptHome));
|
|
5277
5337
|
const backend = sessionBackend(profile, group);
|
|
@@ -6176,9 +6236,11 @@ wss.on("connection", (ws, req) => {
|
|
|
6176
6236
|
}
|
|
6177
6237
|
return m ?? null;
|
|
6178
6238
|
};
|
|
6239
|
+
// The drop user a NEW spawn will run as — same inputs as the isolation
|
|
6240
|
+
// resolution further down. Hoisted out of the MT-token guard because the
|
|
6241
|
+
// worktree below must land in (and be owned by) this same user's home.
|
|
6242
|
+
let earlyUser = null;
|
|
6179
6243
|
if (!resumed) {
|
|
6180
|
-
// A new spawn has no stored group; same inputs as the isolation
|
|
6181
|
-
// resolution further down.
|
|
6182
6244
|
const earlyIso = resolveSpawnIsolation({
|
|
6183
6245
|
mode: DEPLOYMENT_MODE,
|
|
6184
6246
|
resumed,
|
|
@@ -6187,15 +6249,22 @@ wss.on("connection", (ws, req) => {
|
|
|
6187
6249
|
storedGroupId: null,
|
|
6188
6250
|
requestedGroupId: typeof msg.group === "string" && msg.group.trim() ? msg.group.trim() : null,
|
|
6189
6251
|
});
|
|
6190
|
-
|
|
6252
|
+
earlyUser = earlyIso.user ?? commonsOsUser({ migrated: groupsMigrated, mode: DEPLOYMENT_MODE });
|
|
6191
6253
|
const earlyMsg = mtRefusal(earlyUser);
|
|
6192
6254
|
if (earlyMsg)
|
|
6193
6255
|
return fail(earlyMsg);
|
|
6194
6256
|
}
|
|
6195
6257
|
if (msg.worktree && !resumed && isGitRepo(cwd)) {
|
|
6196
6258
|
try {
|
|
6197
|
-
|
|
6259
|
+
// Create the checkout under the DROP USER's own home (not the
|
|
6260
|
+
// server's `/root`, which that user cannot traverse) and hand it to
|
|
6261
|
+
// them, so a dropped agent — notably every Tweak agent — can
|
|
6262
|
+
// actually work in it instead of landing on the invalid-settings
|
|
6263
|
+
// screen. `agentStateHome(null)` is the server home, so mono-tenant
|
|
6264
|
+
// is byte-for-byte unchanged.
|
|
6265
|
+
worktree = createWorktree(cwd, id.slice(0, 8), agentStateHome(earlyUser));
|
|
6198
6266
|
effectiveCwd = worktree.path;
|
|
6267
|
+
await chownAgentStateToDropUser(worktree.path, earlyUser);
|
|
6199
6268
|
}
|
|
6200
6269
|
catch (e) {
|
|
6201
6270
|
return fail("worktree creation failed: " + (e instanceof Error ? e.message : String(e)));
|