@join-pantheon/cli 0.30.0 → 0.31.1
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/README.md +114 -35
- package/dist/agents.js +41 -17
- package/dist/claude-confine.js +37 -21
- package/dist/cli.js +414 -38
- package/dist/client.js +258 -9
- package/dist/command-state.js +25 -2
- package/dist/config.js +79 -7
- package/dist/continued.js +42 -10
- package/dist/doctor.js +93 -15
- package/dist/file-lock.js +197 -0
- package/dist/home-guard.js +372 -17
- package/dist/identity.js +77 -1
- package/dist/mcp-supervisor.js +5 -2
- package/dist/mcp.js +78 -30
- package/dist/nonces.js +130 -0
- package/dist/notifier.js +35 -0
- package/dist/notify.js +9 -3
- package/dist/pending.js +113 -8
- package/dist/pins.js +161 -0
- package/dist/profile.js +19 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +94 -13
- package/dist/reads.js +8 -1
- package/dist/relay.js +208 -35
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +21 -2
- package/dist/run-failure.js +16 -0
- package/dist/service.js +7 -1
- package/dist/setup-guide.js +70 -29
- package/dist/tray.js +55 -4
- package/dist/untrusted.js +24 -5
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +96 -6
- package/dist/worker.js +1059 -93
- package/helper/macos/menubar.swift +245 -105
- package/package.json +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
export function repoName(originOrPath) {
|
|
3
|
+
const s = originOrPath.trim().replace(/[\\/]+$/, "").replace(/\.git$/i, "");
|
|
4
|
+
return safeName(basename(s.replace(/^.*[:/]([^:/]+)$/, "$1")) || s);
|
|
5
|
+
}
|
|
6
|
+
export function safeName(n) {
|
|
7
|
+
return n.normalize("NFKC").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|-+$/g, "").slice(0, 64) || "repo";
|
|
8
|
+
}
|
|
9
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10
|
+
export function names(text, name, origin, strict = false) {
|
|
11
|
+
if (!name)
|
|
12
|
+
return false;
|
|
13
|
+
const t = text.toLowerCase();
|
|
14
|
+
const n = name.toLowerCase();
|
|
15
|
+
if (origin) {
|
|
16
|
+
const slug = origin.toLowerCase().replace(/\.git$/, "").replace(/^.*?[:/]([^:/]+\/[^:/]+)$/, "$1");
|
|
17
|
+
if (slug.includes("/") && t.includes(slug))
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const word = `(?<![\\w./-])${esc(n)}(?![\\w-])`;
|
|
21
|
+
if (n.length >= 5 && !strict && new RegExp(word, "i").test(t))
|
|
22
|
+
return true;
|
|
23
|
+
return new RegExp(`\`${esc(n)}\`|${word}\\s+(?:repo|repository|codebase|app|service|project)\\b|\\b(?:repo|repository|codebase)\\s+${esc(n)}(?![\\w-])|${word}/[\\w.-]`, "i").test(t);
|
|
24
|
+
}
|
|
25
|
+
export function explicitlyNames(text, name) {
|
|
26
|
+
if (!name)
|
|
27
|
+
return false;
|
|
28
|
+
const n = esc(name.toLowerCase());
|
|
29
|
+
const word = `(?<![\\w./-])${n}(?![\\w-])`;
|
|
30
|
+
return new RegExp(`${word}\\s+(?:repo|repository)\\b|\\b(?:repo|repository)\\s+${n}(?![\\w-])`, "i").test(text.toLowerCase());
|
|
31
|
+
}
|
|
32
|
+
export function routeRequest(i) {
|
|
33
|
+
const ws = i.workspaces;
|
|
34
|
+
if (!ws.length)
|
|
35
|
+
return { kind: "none", why: "no folder on this machine is bound to this team" };
|
|
36
|
+
const byFp = (fp) => (fp ? ws.find((w) => w.fp === fp) : undefined);
|
|
37
|
+
const teamName = (fp) => i.teamRepos.find((r) => r.fp === fp)?.name ?? fp;
|
|
38
|
+
const here = ws.map((w) => w.name);
|
|
39
|
+
const localFps = new Set(ws.map((w) => w.fp).filter(Boolean));
|
|
40
|
+
const notHere = i.teamRepos.filter((r) => !localFps.has(r.fp)).map((r) => r.name);
|
|
41
|
+
const max = i.maxContext ?? 2;
|
|
42
|
+
const activity = i.activity ?? (() => 0);
|
|
43
|
+
const withContext = (primary, why, prefer = []) => {
|
|
44
|
+
const others = ws.filter((w) => w !== primary && w.kind === "repo");
|
|
45
|
+
const ordered = [...prefer.filter((w) => w !== primary), ...others.filter((w) => !prefer.includes(w)).sort((a, b) => activity(b) - activity(a))];
|
|
46
|
+
return { kind: "run", primary, context: ordered.filter((w) => w.kind === "repo").slice(0, max), why, notHere };
|
|
47
|
+
};
|
|
48
|
+
const strict = (n) => !!i.teamName && n.toLowerCase() === i.teamName.toLowerCase();
|
|
49
|
+
const namedLocal = ws.filter((w) => names(i.text, w.name, w.origin, strict(w.name))).sort((a, b) => b.name.length - a.name.length);
|
|
50
|
+
const namedTeam = i.teamRepos.filter((r) => !localFps.has(r.fp) && explicitlyNames(i.ask ?? i.text, r.name)).sort((a, b) => b.name.length - a.name.length);
|
|
51
|
+
if (namedLocal.length) {
|
|
52
|
+
return withContext(namedLocal[0], `the request names ${namedLocal.map((w) => w.name).join(" and ")}`, namedLocal.slice(1));
|
|
53
|
+
}
|
|
54
|
+
if (namedTeam.length) {
|
|
55
|
+
return { kind: "missing", name: namedTeam[0].name, why: `the request is about ${namedTeam[0].name}, which is not on this machine`, here };
|
|
56
|
+
}
|
|
57
|
+
const delivered = byFp(i.deliveredRepo);
|
|
58
|
+
if (delivered)
|
|
59
|
+
return withContext(delivered, `this goal's work was delivered in ${delivered.name}`);
|
|
60
|
+
const sender = byFp(i.senderRepo);
|
|
61
|
+
if (sender)
|
|
62
|
+
return withContext(sender, `the request was sent from ${sender.name}`);
|
|
63
|
+
if (ws.length === 1) {
|
|
64
|
+
const note = i.senderRepo && !sender ? ` (it was sent from ${teamName(i.senderRepo)}, which is not on this machine)` : "";
|
|
65
|
+
return { kind: "run", primary: ws[0], context: [], why: `the only folder on this machine bound to this team${note}`, notHere };
|
|
66
|
+
}
|
|
67
|
+
const primary = [...ws].sort((a, b) => activity(b) - activity(a))[0];
|
|
68
|
+
return withContext(primary, `nothing in the request says which, so the most recently active (${primary.name}), with the others open to read`);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=repo-route.js.map
|
package/dist/roster-file.js
CHANGED
|
@@ -230,8 +230,9 @@ export class Allowlist {
|
|
|
230
230
|
if (Date.now() > inv.expires_at)
|
|
231
231
|
return { ok: false, why: "that invite has expired — ask for a fresh one" };
|
|
232
232
|
const existing = this.entries.get(member);
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
const removedAt = this.removedWhen(member);
|
|
234
|
+
if (removedAt !== null && (inv.created_at <= removedAt || (this.founderName && inv.created_by !== this.founderName))) {
|
|
235
|
+
return { ok: false, why: `"${member}" was removed from this team${existing?.revoked_by ? ` by ${existing.revoked_by}` : ""}, so only an invite from the founder${this.founderName ? ` (${this.founderName})` : ""}, made since, can bring them back` };
|
|
235
236
|
}
|
|
236
237
|
if (existing && existing.publicKey !== publicKey) {
|
|
237
238
|
return {
|
|
@@ -251,6 +252,7 @@ export class Allowlist {
|
|
|
251
252
|
};
|
|
252
253
|
this.entries.set(member, entry);
|
|
253
254
|
inv.used_by = member;
|
|
255
|
+
this.clearForgotten(member);
|
|
254
256
|
inv.used_at = Date.now();
|
|
255
257
|
this.flush();
|
|
256
258
|
return { ok: true, entry };
|
|
@@ -346,6 +348,7 @@ export class Allowlist {
|
|
|
346
348
|
if (!e.admitted || e.admitted_by === undefined)
|
|
347
349
|
e.admitted_by = by;
|
|
348
350
|
e.admitted = true;
|
|
351
|
+
this.clearForgotten(member);
|
|
349
352
|
e.may_wake = mayWake;
|
|
350
353
|
delete e.revoked_at;
|
|
351
354
|
delete e.revoked_by;
|
|
@@ -361,8 +364,24 @@ export class Allowlist {
|
|
|
361
364
|
if (i.created_by === member && !i.used_by)
|
|
362
365
|
this.invites.delete(t);
|
|
363
366
|
}
|
|
367
|
+
removedWhen(member) {
|
|
368
|
+
const e = this.entries.get(member);
|
|
369
|
+
if (e?.revoked_at && !e.admitted)
|
|
370
|
+
return e.revoked_at;
|
|
371
|
+
const f = this.extra["forgotten"]?.[member];
|
|
372
|
+
return typeof f === "number" ? f : null;
|
|
373
|
+
}
|
|
374
|
+
clearForgotten(member) {
|
|
375
|
+
const f = this.extra["forgotten"];
|
|
376
|
+
if (f && member in f)
|
|
377
|
+
delete f[member];
|
|
378
|
+
}
|
|
364
379
|
forget(member) {
|
|
365
380
|
const gone = this.entries.delete(member);
|
|
381
|
+
if (gone) {
|
|
382
|
+
const f = (this.extra["forgotten"] ??= {});
|
|
383
|
+
f[member] = Date.now();
|
|
384
|
+
}
|
|
366
385
|
this.cancelInvitesBy(member);
|
|
367
386
|
if (gone) {
|
|
368
387
|
this.removedAt = Date.now();
|
package/dist/run-failure.js
CHANGED
|
@@ -76,6 +76,8 @@ export function diagnoseRun(f, probe = gitProbe) {
|
|
|
76
76
|
lines: [`cat ${shq(f.runLog)}`],
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
+
if (!f.branch && f.folder)
|
|
80
|
+
return silent(f);
|
|
79
81
|
if (!f.branch) {
|
|
80
82
|
return {
|
|
81
83
|
cause: "not_a_repo",
|
|
@@ -85,6 +87,20 @@ export function diagnoseRun(f, probe = gitProbe) {
|
|
|
85
87
|
}
|
|
86
88
|
if (!f.allowBash && !(f.needsCommands || BLOCKED_RE.test(f.transcript ?? f.detail) || BLOCKED_RE.test(f.detail)))
|
|
87
89
|
return silent(f);
|
|
90
|
+
if (!f.allowBash && f.commandsNeedSandbox === "agent-cmd") {
|
|
91
|
+
return {
|
|
92
|
+
cause: "no_sandbox_commands",
|
|
93
|
+
says: "Commands are on here, and this machine is set to keep runs of the agent the worker was started with (--agent-cmd) to edits, so the run edited files only.",
|
|
94
|
+
fix: "To have it run tests, commit and push too: pantheon policy grant unsandboxed-runs — or start the worker without --agent-cmd so Claude Code runs them in its sandbox.",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (!f.allowBash && f.commandsNeedSandbox) {
|
|
98
|
+
return {
|
|
99
|
+
cause: "no_sandbox_commands",
|
|
100
|
+
says: "Commands are on here, and this machine is set to keep runs of this Claude Code (Windows, or older than 2.1.83) to edits, so the run edited files only.",
|
|
101
|
+
fix: "To have it run tests, commit and push too: pantheon policy grant unsandboxed-runs — or update Claude Code to 2.1.83 or later on macOS or Linux, which runs them in its sandbox.",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
88
104
|
if (!f.allowBash) {
|
|
89
105
|
return {
|
|
90
106
|
cause: "no_commands",
|
package/dist/service.js
CHANGED
|
@@ -322,7 +322,8 @@ export function serviceState(kind, exec = realExec, plat = platform(), uid = pro
|
|
|
322
322
|
const dis = outOf(exec, "launchctl", ["print-disabled", `gui/${uid}`]).out;
|
|
323
323
|
const disabled = new RegExp(`"${LABEL(kind).replace(/\./g, "\\.")}"\\s*=>\\s*(disabled|true)`).test(dis);
|
|
324
324
|
const pr = outOf(exec, "launchctl", ["print", `gui/${uid}/${LABEL(kind)}`]);
|
|
325
|
-
|
|
325
|
+
const from = pr.ok ? /^\s*path = (.+)$/m.exec(pr.out)?.[1]?.trim() : undefined;
|
|
326
|
+
return { disabled, loaded: pr.ok, running: pr.ok && /^\s*state = running/m.test(pr.out), ...(from ? { from } : {}) };
|
|
326
327
|
}
|
|
327
328
|
const en = outOf(exec, "systemctl", ["--user", "is-enabled", `pantheon-${kind}`]).out.trim();
|
|
328
329
|
const act = outOf(exec, "systemctl", ["--user", "is-active", `pantheon-${kind}`]).out.trim();
|
|
@@ -338,6 +339,11 @@ export function restartService(kind, exec = realExec, opts = {}) {
|
|
|
338
339
|
return [` ${kind}: left stopped (you stopped it) — start it again: pantheon service install ${kind}`];
|
|
339
340
|
try {
|
|
340
341
|
if (plat === "darwin") {
|
|
342
|
+
if (st.loaded && st.from && resolve(st.from) !== resolve(plistPath(kind)) && existsSync(plistPath(kind))) {
|
|
343
|
+
exec("launchctl", ["bootout", `gui/${uid}/${LABEL(kind)}`]);
|
|
344
|
+
exec("launchctl", ["bootstrap", `gui/${uid}`, plistPath(kind)]);
|
|
345
|
+
return [` ${kind}: was running from ${st.from}, not this machine's own definition — reloaded from ${plistPath(kind)} on the new code`];
|
|
346
|
+
}
|
|
341
347
|
if (st.loaded && !st.running)
|
|
342
348
|
return [` ${kind}: not running — left as it is (launchd starts it again on its own schedule)`];
|
|
343
349
|
if (!st.loaded) {
|
package/dist/setup-guide.js
CHANGED
|
@@ -7,6 +7,7 @@ import { applyProfileInit, loadProfile, profilePath, saveProfile, wakeMode } fro
|
|
|
7
7
|
import { installService as realInstall, uninstallService as realUninstall, serviceInstalled as realInstalled } from "./service.js";
|
|
8
8
|
import { personaDir } from "./persona.js";
|
|
9
9
|
import { hudBinaryPath } from "./notifier.js";
|
|
10
|
+
import { grant, inWokenRun, loadPolicy, revoke, savePolicy, unsandboxedAllowed } from "./worker-policy.js";
|
|
10
11
|
export const DECISION_AREAS = [
|
|
11
12
|
{ id: "product", label: "Product — what we build, for whom, and what goes in a release" },
|
|
12
13
|
{ id: "engineering", label: "Engineering — architecture, and changes that cross team lines" },
|
|
@@ -19,8 +20,8 @@ export const DECISION_AREAS = [
|
|
|
19
20
|
];
|
|
20
21
|
export const TITLE_OPTIONS = ["Founder", "Engineer", "Designer", "Product", "Operations", "Something else"];
|
|
21
22
|
export const WAKE_OPTIONS = [
|
|
22
|
-
{ id: "anyone", label: "Yes — anyone on my teams. Invites you send grant it;
|
|
23
|
-
{ id: "approved", label: "Only people I approve
|
|
23
|
+
{ id: "anyone", label: "Yes — anyone on my teams (the default). Invites you send grant it; to stop one person later, switch to \"only people I approve\" and pantheon wake deny <name>." },
|
|
24
|
+
{ id: "approved", label: "Only people I approve — I am asked the first time each one's request needs this machine, or approve ahead with pantheon wake allow <name>; invites never grant it." },
|
|
24
25
|
{ id: "no", label: "No — this machine only works while I am at it." },
|
|
25
26
|
];
|
|
26
27
|
export const ALERT_OPTIONS = [
|
|
@@ -42,6 +43,7 @@ export function guideDefaults(profile, cfg) {
|
|
|
42
43
|
persona: false,
|
|
43
44
|
};
|
|
44
45
|
}
|
|
46
|
+
const andList = (xs) => xs.length < 2 ? xs.join("") : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
45
47
|
export function optOutsFrom(argv) {
|
|
46
48
|
return { worker: argv.includes("--no-worker"), notifications: argv.includes("--no-notifications") };
|
|
47
49
|
}
|
|
@@ -67,12 +69,11 @@ export function questionnaire() {
|
|
|
67
69
|
"Ask your human in plain words, then pass the answers as flags — to `pantheon setup --yes`, or straight to",
|
|
68
70
|
"`pantheon start` / `pantheon join`, which apply them the moment the team exists.",
|
|
69
71
|
"",
|
|
70
|
-
"Only 3 and 4 need the person
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"Without a repo, --wake, --commands, --playbook and --base do nothing yet: skip them and say so.",
|
|
72
|
+
"Only 3 and 4 need the person. The rest have defaults you may take and say out loud:",
|
|
73
|
+
" --wake anyone --commands yes --decides none --alerts decisions --playbook yes --base <what origin calls its default> --approve-all",
|
|
74
|
+
"--wake anyone (the default) means any teammate the team lets wake workers can start a run here while they are away; --commands yes (the default in a repo) means that run may run tests, commit and push to its own branch.",
|
|
75
|
+
"Codex, Cursor and Copilot answer those requests too, so teammates are never kept waiting; --unsandboxed no makes them wait for the person instead.",
|
|
76
|
+
"--commands, --playbook and --base are about a repo: skip them where nothing bound here is one (a docs folder), and say so.",
|
|
76
77
|
"",
|
|
77
78
|
"1. While they're away, may teammates hand work to this machine? (their agent runs here on its own,",
|
|
78
79
|
" in a separate copy of the repo, and delivers on a branch for review)",
|
|
@@ -122,6 +123,9 @@ export function parseSetupFlags(argv) {
|
|
|
122
123
|
const commands = yn("commands");
|
|
123
124
|
if (commands !== undefined)
|
|
124
125
|
preset.commands = commands;
|
|
126
|
+
const unsandboxed = yn("unsandboxed");
|
|
127
|
+
if (unsandboxed !== undefined)
|
|
128
|
+
preset.unsandboxed = unsandboxed;
|
|
125
129
|
const name = val("name");
|
|
126
130
|
if (name !== undefined)
|
|
127
131
|
preset.name = name.trim();
|
|
@@ -160,6 +164,7 @@ export function parseSetupFlags(argv) {
|
|
|
160
164
|
}
|
|
161
165
|
export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
162
166
|
const optOut = ctx.optOut ?? {};
|
|
167
|
+
const serves = ctx.project ? { path: ctx.project, repos: true } : ctx.serves ?? null;
|
|
163
168
|
const os = deps.platform ?? process.platform;
|
|
164
169
|
const mac = os === "darwin";
|
|
165
170
|
const install = deps.installService ?? realInstall;
|
|
@@ -168,29 +173,27 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
168
173
|
const file = deps.profileFile ?? profilePath();
|
|
169
174
|
const profile = loadProfile(ctx.cfg, file);
|
|
170
175
|
const d = { ...guideDefaults(profile, ctx.cfg), ...preset };
|
|
176
|
+
if (profile.agent.allow_bash === undefined && preset.commands === undefined && serves?.repos)
|
|
177
|
+
d.commands = true;
|
|
171
178
|
const a = { ...d };
|
|
172
179
|
if (a.wake === "no") {
|
|
173
180
|
a.commands = false;
|
|
174
181
|
a.persona = false;
|
|
175
182
|
}
|
|
176
183
|
const applied = [], warnings = [];
|
|
184
|
+
const awayAgents = deps.unsandboxedAgents?.() ?? [];
|
|
177
185
|
if (io) {
|
|
178
|
-
io.say("
|
|
186
|
+
io.say("A few questions, then one screen of defaults. Enter keeps the suggested answer.\n");
|
|
179
187
|
a.name = (await io.ask("Your name, as teammates will see it", d.name)).trim() || d.name;
|
|
180
188
|
const known = TITLE_OPTIONS.findIndex((t) => t.toLowerCase() === d.title.toLowerCase());
|
|
181
|
-
const t = await io.choose("Your role, in a word — it goes on the team directory", TITLE_OPTIONS, known >= 0 ? known : d.title ? TITLE_OPTIONS.length - 1 : 0);
|
|
189
|
+
const t = await io.choose("Your role, in a word — it goes on the team directory", TITLE_OPTIONS, known >= 0 ? known : d.title || !ctx.founder ? TITLE_OPTIONS.length - 1 : 0);
|
|
182
190
|
a.title = t === TITLE_OPTIONS.length - 1 ? (await io.ask("Your role", d.title)).trim() || d.title || "Member" : TITLE_OPTIONS[t];
|
|
183
191
|
const askCommands = async () => {
|
|
184
192
|
const undecided = profile.agent.allow_bash === undefined && preset.commands === undefined;
|
|
185
|
-
const c = await io.choose("Teammates can hand work to this machine while you're away. It runs in a separate copy of the repo and delivers on a branch you review. May a run also
|
|
193
|
+
const c = await io.choose("Teammates can hand work to this machine while you're away. It runs in a separate copy of the repo and delivers on a branch you review. May a run also run your tests, commit and push, so the work arrives ready to review?", ["Yes — run tests, commit, and push work for review (recommended)",
|
|
186
194
|
"No — edit files and answer only; it cannot run tests, commit, or push"], undecided || a.commands ? 0 : 1);
|
|
187
195
|
a.commands = c === 0;
|
|
188
196
|
};
|
|
189
|
-
let commandsAsked = false;
|
|
190
|
-
if (a.wake !== "no" && ctx.project) {
|
|
191
|
-
await askCommands();
|
|
192
|
-
commandsAsked = true;
|
|
193
|
-
}
|
|
194
197
|
const detected = ctx.project && deps.setRepoBase ? detectBaseBranch(ctx.project) : null;
|
|
195
198
|
const suggestedBase = detected ? (preset.base ?? detected) : undefined;
|
|
196
199
|
if (a.wake !== "no" && ctx.project)
|
|
@@ -198,9 +201,18 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
198
201
|
const shortAlert = (id) => ALERT_OPTIONS.find((o) => o.id === id).label.split(" — ")[0].replace(/ \(recommended\)/, "");
|
|
199
202
|
io.say("\nThe rest has defaults. Change them now, or any time with `pantheon setup`:");
|
|
200
203
|
io.say(` • Wake: ${a.wake === "no" ? "off — this machine only works while you are at it"
|
|
201
|
-
: !
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
: serves && !serves.repos ? (a.wake === "anyone"
|
|
205
|
+
? "teammates can ask your worker about this folder while you're away — it reads it, never changes it"
|
|
206
|
+
: "teammates you approve can ask your worker about this folder while you're away — it reads it, never changes it; you are asked the first time each one needs it")
|
|
207
|
+
: !serves ? "nothing is bound to this team on this machine yet, so there is nothing to serve — bind a repo or a docs folder: pantheon use <team> --here"
|
|
208
|
+
: a.wake === "anyone" ? "your teammates can hand work to this machine while you're away, so nothing waits on you (to approve each one first: pantheon setup --wake approved)"
|
|
209
|
+
: "only people you approve — you are asked the first time a teammate's request needs this machine (Always / Just this once / No)"}`);
|
|
210
|
+
if (a.wake !== "no" && serves?.repos) {
|
|
211
|
+
io.say(` • Commands: ${a.commands ? "work runs your tests, commits, and pushes to its own branch, so it arrives ready for your review" : "edits and answers only — turn commands on to have work arrive tested and ready to review"}`);
|
|
212
|
+
if (awayAgents.length && unsandboxedAllowed(loadPolicy(deps.policyHome))) {
|
|
213
|
+
io.say(` • Agent: ${andList(awayAgents)} ${awayAgents.length === 1 ? "answers" : "answer"} teammates' requests while you're away, so nobody waits on you`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
204
216
|
io.say(` • Alerts: ${shortAlert(a.notifications).toLowerCase()}`);
|
|
205
217
|
io.say(` • Approval: ${a.approval === "approve"
|
|
206
218
|
? "your agent sends to teammates in your name without asking each time (Claude Code, Cursor, Codex, and Kiro's IDE and pantheon agent; Copilot asks once per tool per folder)"
|
|
@@ -222,9 +234,10 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
222
234
|
a.commands = false;
|
|
223
235
|
a.persona = false;
|
|
224
236
|
}
|
|
225
|
-
else if (
|
|
237
|
+
else if (serves?.repos) {
|
|
226
238
|
await askCommands();
|
|
227
|
-
|
|
239
|
+
if (ctx.project)
|
|
240
|
+
a.persona = preset.persona ?? true;
|
|
228
241
|
}
|
|
229
242
|
const picks = await io.chooseMany("Which of these do you decide personally? Agents — yours and your teammates' — will stop and ask you before acting on them. Pick any number, or none.", DECISION_AREAS.map((x) => x.label), a.decides.map((id) => DECISION_AREAS.findIndex((x) => x.id === id)).filter((i) => i >= 0));
|
|
230
243
|
a.decides = [...new Set(picks.map((i) => DECISION_AREAS[i].id))];
|
|
@@ -307,22 +320,50 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
307
320
|
else
|
|
308
321
|
applied.push("worker stays off");
|
|
309
322
|
}
|
|
310
|
-
else if (
|
|
323
|
+
else if (serves) {
|
|
311
324
|
try {
|
|
312
|
-
install("worker",
|
|
313
|
-
applied.push(
|
|
314
|
-
? "worker on — a
|
|
315
|
-
:
|
|
325
|
+
install("worker", serves.path, a.commands ? ["--allow-bash"] : []);
|
|
326
|
+
applied.push(!serves.repos
|
|
327
|
+
? "worker on — a teammate's question is answered from your folder, which a run reads and never changes; anything it writes is a draft for you"
|
|
328
|
+
: a.commands
|
|
329
|
+
? "worker on — a woken run may run tests and commit and push to its own branch"
|
|
330
|
+
: "worker on — a woken run edits files and answers; it cannot commit or push (deliveries will need you)");
|
|
316
331
|
}
|
|
317
332
|
catch (e) {
|
|
318
333
|
warnings.push(`the worker service did not install: ${e.message}`);
|
|
319
334
|
}
|
|
320
335
|
applied.push(a.wake === "anyone"
|
|
321
|
-
? "wake permission: invites you send
|
|
322
|
-
: "wake permission: only people you approve on this machine (pantheon wake allow <name>)");
|
|
336
|
+
? "wake permission: your teammates can hand you work while you're away, and invites you send include it (to approve each one first: pantheon setup --wake approved)"
|
|
337
|
+
: "wake permission: only people you approve on this machine — you are asked the first time each one needs it (approve ahead: pantheon wake allow <name>)");
|
|
323
338
|
}
|
|
324
339
|
else {
|
|
325
|
-
applied.push(
|
|
340
|
+
applied.push(`worker: nothing is bound to "${ctx.cfg.space}" on this machine yet — from the folder you work in (a repo, or a folder of docs): pantheon use ${ctx.cfg.space} --here, then pantheon setup`);
|
|
341
|
+
}
|
|
342
|
+
const names = awayAgents.length ? andList(awayAgents) : "Codex, Cursor and Copilot";
|
|
343
|
+
const one = awayAgents.length === 1;
|
|
344
|
+
if (a.unsandboxed !== undefined && !(a.unsandboxed && a.wake === "no")) {
|
|
345
|
+
if (inWokenRun())
|
|
346
|
+
warnings.push("a woken run cannot change what runs on this machine — a person has to answer this here");
|
|
347
|
+
else {
|
|
348
|
+
try {
|
|
349
|
+
const pol = loadPolicy(deps.policyHome);
|
|
350
|
+
if (a.unsandboxed)
|
|
351
|
+
grant(pol, "unsandboxed_runs", "always", "setup-answer");
|
|
352
|
+
else
|
|
353
|
+
revoke(pol, "unsandboxed_runs");
|
|
354
|
+
savePolicy(pol, deps.policyHome);
|
|
355
|
+
applied.push(a.unsandboxed
|
|
356
|
+
? `${names} ${one ? "answers" : "answer"} teammates' requests while you're away, so nobody waits on you — to have them wait for you instead: pantheon setup --unsandboxed no`
|
|
357
|
+
: `${names} ${one ? "leaves" : "leave"} teammates' requests for you to pick up when you're back — to have ${names} answer them while you're away: pantheon setup --unsandboxed yes`);
|
|
358
|
+
}
|
|
359
|
+
catch (e) {
|
|
360
|
+
warnings.push(`could not save whether ${names} may take requests while you're away: ${e.message}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else if (awayAgents.length && a.wake !== "no" && serves?.repos) {
|
|
365
|
+
if (unsandboxedAllowed(loadPolicy(deps.policyHome)))
|
|
366
|
+
applied.push(`${names} ${one ? "answers" : "answer"} teammates' requests while you're away, so nobody waits on you — to have them wait for you instead: pantheon setup --unsandboxed no`);
|
|
326
367
|
}
|
|
327
368
|
const waitIn = mac ? "pantheon status, approvals, and the menu bar" : "pantheon status and pantheon approvals";
|
|
328
369
|
const chosenAlerts = ALERT_OPTIONS.find((o) => o.id === a.notifications).label.split(" — ")[0].replace(/ \(recommended\)/, "");
|
package/dist/tray.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadPolicy } from "./worker-policy.js";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { helperStale, recordHelperBuild } from "./helpers-build.js";
|
|
3
4
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -51,6 +52,51 @@ export function writeTrayCommands(home = CONFIG_DIR) {
|
|
|
51
52
|
}
|
|
52
53
|
return out;
|
|
53
54
|
}
|
|
55
|
+
const WEEK_MS = 7 * 24 * 3600_000;
|
|
56
|
+
const RANK = (i) => (i.kind === "decision" ? 0 : 1);
|
|
57
|
+
export const boardOrder = (a, b) => RANK(a) - RANK(b) || a.when - b.when;
|
|
58
|
+
export function arrangeBoard(items, o) {
|
|
59
|
+
const fallback = o.fallbackMs ?? LIMITS.workerFallbackMs;
|
|
60
|
+
const out = items.map((i) => ({ ...i }));
|
|
61
|
+
const spaceOf = (i) => i.space ?? "";
|
|
62
|
+
const asked = new Map();
|
|
63
|
+
for (const d of out)
|
|
64
|
+
if (d.kind === "decision" && d.about && (d.who === "your agent" || d.choices))
|
|
65
|
+
asked.set(`${spaceOf(d)}|${d.about}`, d);
|
|
66
|
+
for (const i of out) {
|
|
67
|
+
if (i.kind === "decision") {
|
|
68
|
+
i.lane = "now";
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (i.kind === "waiting") {
|
|
72
|
+
i.lane = "waiting";
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const d = i.ref ? asked.get(`${spaceOf(i)}|${i.ref}`) : undefined;
|
|
76
|
+
if (d) {
|
|
77
|
+
d.context ??= i.headline;
|
|
78
|
+
i.lane = undefined;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const age = Math.max(0, o.now - i.when);
|
|
82
|
+
if (i.claimed || (o.workerRunning && age < fallback)) {
|
|
83
|
+
i.lane = "handling";
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
i.lane = age > WEEK_MS ? "older" : "now";
|
|
87
|
+
}
|
|
88
|
+
return out.filter((i) => i.lane).sort(boardOrder);
|
|
89
|
+
}
|
|
90
|
+
export function localChoices(ref) {
|
|
91
|
+
if (!ref)
|
|
92
|
+
return null;
|
|
93
|
+
const pol = loadPolicy();
|
|
94
|
+
if (pol.wakeAsks?.[ref])
|
|
95
|
+
return [{ label: "Always", answer: "allow always" }, { label: "Just this once", answer: "allow once" }, { label: "No", answer: "deny" }];
|
|
96
|
+
if (Object.values(pol.asks).some((a) => a?.act === ref))
|
|
97
|
+
return [{ label: "Allow always", answer: "allow always" }, { label: "Allow once", answer: "allow once" }, { label: "Don't allow", answer: "deny" }];
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
54
100
|
function workerState(home = CONFIG_DIR) {
|
|
55
101
|
try {
|
|
56
102
|
const raw = JSON.parse(readFileSync(join(home, "worker-state.json"), "utf8"));
|
|
@@ -104,7 +150,7 @@ export async function trayStatus(cfg = load(), f = loadAll()) {
|
|
|
104
150
|
members: ok.reduce((n, p) => n + (p.members ?? 0), 0),
|
|
105
151
|
decisions: ok.flatMap((p) => (p.decisions ?? []).map((d) => ({ ...d, body: `[${p.space}] ${d.body}` }))),
|
|
106
152
|
decisions_prompt: allDecisionsPrompt(ok.reduce((n, p) => n + (p.decisions?.length ?? 0), 0)),
|
|
107
|
-
items: ok.flatMap((p) => (p.items ?? []).map((i) => ({ ...i, space: p.space }))),
|
|
153
|
+
items: ok.flatMap((p) => (p.items ?? []).map((i) => ({ ...i, space: p.space }))).sort(boardOrder),
|
|
108
154
|
owed_count: ok.reduce((n, p) => n + (p.owed_count ?? 0), 0),
|
|
109
155
|
waiting_count: ok.reduce((n, p) => n + (p.waiting_count ?? 0), 0),
|
|
110
156
|
};
|
|
@@ -132,15 +178,18 @@ async function trayStatusOne(cfg) {
|
|
|
132
178
|
const row = (o, kind, who, prompt) => ({
|
|
133
179
|
id: o.id, kind, title: decisionTitle(o.body), headline: decisionTitle(o.body, 200), detail: o.body, prompt, who,
|
|
134
180
|
when: o.created_at ?? 0, space: cfg.space,
|
|
181
|
+
...(o.claimed ? { claimed: true } : {}),
|
|
135
182
|
...(o.goal ? { goal: o.goal } : {}),
|
|
136
183
|
...(o.ref ? { ref: o.ref } : {}),
|
|
184
|
+
...(typeof o.about === "string" && o.about ? { about: o.about } : {}),
|
|
137
185
|
});
|
|
138
186
|
const owed = all.filter((o) => o.owner === cfg.member);
|
|
139
187
|
const waiting = all.filter((o) => o.owner !== cfg.member && o.owner !== `human:${cfg.member}`);
|
|
140
188
|
const items = [
|
|
141
189
|
...mineHuman.map((o) => {
|
|
142
190
|
const from = senderOf(o);
|
|
143
|
-
|
|
191
|
+
const choices = localChoices(o.ref);
|
|
192
|
+
return { ...row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id, decisionTitle(o.body))), decider: cfg.member, ...(choices ? { choices } : {}) };
|
|
144
193
|
}),
|
|
145
194
|
...owed.map((o) => row(o, "owed", "you", owedPrompt(decisionTitle(o.body), o.goal))),
|
|
146
195
|
...waiting.map((o) => {
|
|
@@ -151,6 +200,8 @@ async function trayStatusOne(cfg) {
|
|
|
151
200
|
: r;
|
|
152
201
|
}),
|
|
153
202
|
];
|
|
203
|
+
const workerRunning = workerProc().running;
|
|
204
|
+
const board = arrangeBoard(items, { now: Date.now(), workerRunning });
|
|
154
205
|
const profile = loadProfile(cfg);
|
|
155
206
|
const surface = profile.agent.primary_surface;
|
|
156
207
|
const surfaceName = surface === "claude-code" ? "Claude" : surface === "cursor" ? "Cursor" : surface === "copilot" ? "Copilot" : surface === "kiro" ? "Kiro" : "Codex";
|
|
@@ -167,7 +218,7 @@ async function trayStatusOne(cfg) {
|
|
|
167
218
|
unavailable_spaces: !space || !decisionsOk ? [cfg.space] : [],
|
|
168
219
|
online: space?.online ?? 0,
|
|
169
220
|
members: space?.members ?? 0,
|
|
170
|
-
worker_running:
|
|
221
|
+
worker_running: workerRunning,
|
|
171
222
|
worker_installed: serviceInstalled("worker"),
|
|
172
223
|
worker_plist: plistPath("worker"),
|
|
173
224
|
notifier_running: notifierProc().running,
|
|
@@ -177,7 +228,7 @@ async function trayStatusOne(cfg) {
|
|
|
177
228
|
surface_bundle_id: SURFACE_BUNDLE_IDS[surface],
|
|
178
229
|
decisions_prompt: allDecisionsPrompt(decisions.length),
|
|
179
230
|
decisions,
|
|
180
|
-
items,
|
|
231
|
+
items: board,
|
|
181
232
|
owed_count: owed.length,
|
|
182
233
|
waiting_count: waiting.length,
|
|
183
234
|
};
|
package/dist/untrusted.js
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
|
+
const SECRET_NOUN = String.raw `(?:passwords?|passphrases?|pass ?codes?|pins?|secrets?|secret\s*keys?|(?:signing|webhook|client|app|master|root|encryption) (?:secrets?|keys?)|key_secret|secret_key|client_secret|access tokens?|personal access tokens?|session (?:cookies?|ids?|tokens?)|cookies?|dsns?|a-?p-?i[_ -]?keys?|private[_ -]?keys?|ssh[_ -]?keys?|deploy keys?|keys? for [\w.-]+|credentials?|creds|tokens?|otps?|one[- ]time (?:codes?|passwords?)|seed phrases?|recovery codes?|2fa codes?|clave(?: api)?|contraseñas?|passwort|mot de passe|senha)`;
|
|
2
|
+
const VALUE = String.raw `(?:\b(?:your|the|our|my|its|this|that|their|his|her|apna|apni)\s+(?:[\w'-]+\s+){0,3}${SECRET_NOUN}\b|\b[A-Z][A-Z0-9_]*_(?:KEY|SECRET|TOKEN|PASSWORD|PASS|CREDENTIALS?|URL|DSN|URI)\b|\b(?:TOKEN|SECRET|PASSWORD|API_KEY)\b|\$[A-Z_]{3,}|process\.env\.\w+|~\/\.(?:aws|ssh|config|netrc|npmrc|pantheon|docker|kube)\b|\.env\b|id_[er]sa|\.pem\b|\bcredentials file\b)`;
|
|
3
|
+
const HAND_OVER = String.raw `(?:bhej(?:o| do| dena)?|de do|dedo|batao|bata do|dikhao|paste karo|share karo|what(?:'s| is| are)|tell(?: me)?|let (?:me|us) know|give(?: me)?|send(?: me| over| it)?|share|paste|reveal|print|show|dump|echo|cat|read(?: out)?|include|put|drop|post|copy|grab|fetch|get|commit|push|upload|e-?mail|dm|reply with|respond with|need|type|enter|use|using|provide|leak|export)`;
|
|
4
|
+
const SECRET_ACTION = new RegExp(String.raw `\b${HAND_OVER}\b[^\n.?!]{0,60}${VALUE}|${VALUE}[^\n.?!]{0,40}\b(?:in|into|to|should go in|goes in|belongs in)\b[^\n.?!]{0,12}\b(?:reply|answer|message|response|output|commit|branch|repo|file|pr|pull request|issue|thread|description|chat|comment|gist|paste)\b|\b(?:its|the) value\b|\bvalue of (?:the |your )?${SECRET_NOUN}|${VALUE}[^\n?!]{0,40}\b(?:send(?: it)? over|send it|share it|paste it|give it|need it|let me know|bhej(?:o| do)?|de do|batao|dikhao|karo|kar do)\b|${SECRET_NOUN}[\s\S]{0,200}\b(?:print|paste|send|share|show|dump|echo|reveal|include|post|email|bhej(?:o| do)?)\s+(?:it|them|that|this|those)\b|\b(?:create|generate|make|set up|register|mint|issue)\b\s+(?:(?:a|an|the|your|new|brand-new|fresh|another)\s+)*(?:[\w-]+\s+){0,4}${SECRET_NOUN}\b(?!\s+(?:field|flow|screen|page|form|meter|policy|reset|rotation|storage|validation|check|rule|input|button|endpoint|api|feature|generator|manager|strength|expiry|hashing|column|table|model|schema|test|tests|ui|modal))`, "i");
|
|
5
|
+
const SIGNIN_ACTION = /(^|[.!?:;]\s*|\n\s*|\b(?:please|you|can you|could you|would you|go and|go|then|and|now|first|also)\s+)(sign in|sign into|log ?in(?:to)?|authenticate|create an account|join .{0,20}hub)(?:\s+(?:to|into|at|here|there|as|against|on)\b|\s*[.!]|$)|\b(?:visit|open|go to)\b[^\n]{0,40}https?:\/\/|\b(?:with|using) (?:the|our|my|your|an?|this) [\w -]{0,20}(?:account|login|creds|credentials|password)\b|\b(?:gh|npm|docker|aws|gcloud|az|vercel|railway|heroku|firebase) (?:auth )?login\b|(?:^|[.!?:;]\s*|\n\s*)(?:log ?into|sign into|log in to|sign in to)\b(?![^\n.?!]{0,50}\b(?:fails?|failing|broken|error|doesn'?t|not work|is slow))/im;
|
|
6
|
+
const ABOUT_PRODUCT = /\?|\b(?:why|how come|does|do|did|is|are|was|were|where|when|which)\b[^\n]{0,80}\b(?:fail\w*|break\w*|broken|work\w*|return\w*|call\w*|read\w*|stor\w*|handl\w*|show\w*|happen\w*|arriv\w*|expire\w*)|\b(?:fails?|failing|failed|broken|bug|error|errors|issue|crash\w*|doesn'?t work|not working|never arrives?|stopped working|is slow|can'?t|cannot|unable to|won'?t|isn'?t|aren'?t|expires? too|too early|too soon|times? out|timed out)\b/i;
|
|
7
|
+
const WORK_ON_PRODUCT = /(^|[.!?:;]\s*|\n\s*|\b(?:please|can you|could you|let's|we need to|we should)\s+)(?:add|implement|build|create|design|refactor|migrate|move|document|support|fix|update|improve|remove|rename|review|explain|describe|test|write tests for|validate|hash|encrypt|rotate|mask|hide|redact|make)\b/im;
|
|
8
|
+
const NOT_ENGLISH = /[^\x00-\x7F‐-‟… -ÿ]|\b(?:envíame|dame|mándame|schick|envoie|donne-moi|manda)\b/i;
|
|
9
|
+
function secretStops(body) {
|
|
10
|
+
if (SECRET_ACTION.test(body) || SIGNIN_ACTION.test(body))
|
|
11
|
+
return true;
|
|
12
|
+
if (NOT_ENGLISH.test(body) && new RegExp(SECRET_NOUN, "i").test(body))
|
|
13
|
+
return true;
|
|
14
|
+
return !(ABOUT_PRODUCT.test(body) || WORK_ON_PRODUCT.test(body));
|
|
15
|
+
}
|
|
1
16
|
const PATTERNS = [
|
|
2
17
|
{ kind: "install", why: "asks you to install software", re: /\b(npm|pnpm|yarn)\s+(i|install|add)\s+(-g|--global)/i },
|
|
3
18
|
{ kind: "install", why: "asks you to install software", re: /\b(brew|apt-get|apt|pip3?|gem)\s+install\b/i },
|
|
4
19
|
{ kind: "pipe-to-shell", why: "pipes a download straight into a shell", re: /\bcurl\b[^\n|]*\|\s*(ba)?sh\b/i },
|
|
5
20
|
{ kind: "pipe-to-shell", why: "pipes a download straight into a shell", re: /\bwget\b[^\n|]*\|\s*(ba)?sh\b/i },
|
|
6
|
-
{ kind: "credential", why: "asks for a password, token or key", re:
|
|
7
|
-
{ kind: "credential", why: "names an env var that usually holds a secret", re: /\b[A-
|
|
21
|
+
{ kind: "credential", why: "asks for a password, token or key", re: new RegExp(String.raw `\b(?:password|passphrase|secret\s*key|signing secret|api[_ -]?key|a-p-i key|private[_ -]?key|ssh[_ -]?key|credential|creds|otp|seed phrase|recovery code|clave|contraseña|passwort|mot de passe|secret|token|cookie|dsn|(?:signing|webhook|client|app|master|root|encryption) key|key_secret|secret_key|client_secret)s?\b|process\.env\.\w+|\.env\b|~\/\.aws\b|\bthe (?:\w+ )?(?:secret|token|key for \w+)\b`, "i"), stopWhen: secretStops },
|
|
22
|
+
{ kind: "credential", why: "names an env var that usually holds a secret", re: /\b[A-Z][A-Z0-9_]*(TOKEN|SECRET|PASSWORD|API_KEY|_KEY|_PASS|_CREDENTIALS?)\b|\b(TOKEN|SECRET|PASSWORD|API_KEY)\b/, stopWhen: secretStops },
|
|
8
23
|
{ kind: "exfiltration", why: "asks you to send file contents somewhere", re: /\b(cat|base64)\b[^\n]{0,40}\b(curl|POST|upload|send)\b/i },
|
|
9
24
|
{ kind: "exfiltration", why: "asks you to reveal the contents of a sensitive file",
|
|
10
|
-
re: /\b(paste|show|print|read|send|share|output|dump)\b[^\n]{0,60}(~\/\.\w+|\.env\b|config\.json|id_[er]sa|\.ssh
|
|
25
|
+
re: /\b(paste|show|print|read|send|share|output|dump)\b[^\n]{0,60}(~\/\.\w+|\.env\b|config\.json|id_[er]sa|\.ssh\/|\/credentials?\b|credentials? file|\.pem\b)/i },
|
|
26
|
+
{ kind: "exfiltration", why: "asks you to read out a file that holds secrets",
|
|
27
|
+
re: /\b(cat|type|less|more|head|tail|open|read|print|dump|include|paste|show|copy|send|upload|grep|base64)\b[^\n]{0,40}(~\/\.(aws|ssh|config|netrc|npmrc|docker|kube|pantheon|gnupg)\b|\.env\b|id_[er]sa|\.pem\b|\/credentials\b|\.git-credentials|google-services\.json|GoogleService-Info\.plist|service-account[\w.-]*\.json|\.p12\b|\.keystore\b|\.jks\b)/i },
|
|
28
|
+
{ kind: "exfiltration", why: "asks you to dump the environment", re: /\b(printenv|env\s*\||export -p|set\s*\|)|`env`|\bprocess\.env\b[^\n]{0,20}\b(print|log|dump|send|paste|json)/i },
|
|
29
|
+
{ kind: "exfiltration", why: "asks you to send something to an outside address", re: /\b(e-?mail|send|upload|post|forward|share|mail)\b[^\n]{0,80}\b(to|at)\s+(?:[\w.+-]+@[\w-]+\.[\w.-]+|https?:\/\/)/i },
|
|
11
30
|
{ kind: "exfiltration", why: "names a file that usually holds secrets",
|
|
12
31
|
re: /(~\/\.pantheon\/config\.json|~\/\.ssh\/|\.env\.(local|production)|id_rsa\b)/i },
|
|
13
|
-
{ kind: "identity", why: "asks you to sign in or create an account", re: /\b(sign in|log ?in
|
|
32
|
+
{ kind: "identity", why: "asks you to sign in or create an account", re: /\b(sign in|sign into|log ?in(?:to)?|authenticate|create an account|join .{0,20}hub|auth login)\b/i, stopWhen: secretStops },
|
|
14
33
|
{ kind: "override", why: "tries to override the instructions you were given", re: /\b(ignore|disregard|forget)\b[^\n]{0,30}\b(previous|prior|earlier|above|your)\b[^\n]{0,20}\b(instruction|prompt|rule|direction)s?\b/i },
|
|
15
34
|
{ kind: "override", why: "claims a new operator or system directive", re: /\b(system|operator|admin)\s*(:|message|override|instruction)/i },
|
|
16
35
|
];
|
|
@@ -34,7 +53,7 @@ export function assess(body) {
|
|
|
34
53
|
if (seen.has(key))
|
|
35
54
|
continue;
|
|
36
55
|
seen.add(key);
|
|
37
|
-
out.push({ kind: p.kind, why: p.why, evidence: hit[0].slice(0, 80) });
|
|
56
|
+
out.push({ kind: p.kind, why: p.why, evidence: hit[0].slice(0, 80), stop: p.stopWhen ? p.stopWhen(body) : true });
|
|
38
57
|
}
|
|
39
58
|
return out;
|
|
40
59
|
}
|
package/dist/upgrade.js
CHANGED
|
@@ -70,7 +70,7 @@ export function defaultDeps() {
|
|
|
70
70
|
catch { }
|
|
71
71
|
return up;
|
|
72
72
|
},
|
|
73
|
-
npmUpgrade: () => npm(["install", "-g", `${PKG_NAME}
|
|
73
|
+
npmUpgrade: (version) => npm(["install", "-g", `${PKG_NAME}@${version ?? "latest"}`, "--no-audit", "--no-fund"], { stdio: ["ignore", "pipe", "pipe"] }),
|
|
74
74
|
npmLatest: () => {
|
|
75
75
|
try {
|
|
76
76
|
return npm(["view", `${PKG_NAME}`, "version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim() || null;
|
|
@@ -81,18 +81,22 @@ export function defaultDeps() {
|
|
|
81
81
|
},
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
-
export function upgrade(deps = defaultDeps()) {
|
|
84
|
+
export function upgrade(deps = defaultDeps(), opts = {}) {
|
|
85
|
+
const spec = opts.version && /^\d+\.\d+\.\d+$/.test(opts.version) ? opts.version : "latest";
|
|
85
86
|
const { root } = deps;
|
|
86
87
|
const from = deps.version(root);
|
|
87
88
|
const steps = [];
|
|
88
89
|
const stop = (name, detail) => ({ root, from, to: from, steps: [...steps, { name, ok: false, detail }], ok: false, alreadyCurrent: false, restart: [] });
|
|
89
90
|
if (!deps.isGit(root)) {
|
|
90
91
|
try {
|
|
91
|
-
deps.npmUpgrade();
|
|
92
|
-
steps.push({ name: "npm", ok: true, detail: `installed ${PKG_NAME}
|
|
92
|
+
deps.npmUpgrade(spec === "latest" ? undefined : spec);
|
|
93
|
+
steps.push({ name: "npm", ok: true, detail: `installed ${PKG_NAME}@${spec}` });
|
|
93
94
|
}
|
|
94
95
|
catch (e) {
|
|
95
|
-
|
|
96
|
+
if (spec !== "latest" && /ETARGET|No matching version|notarget/i.test(String(e.stderr ?? "") + e.message)) {
|
|
97
|
+
return { root, from, to: from, steps: [...steps, { name: "npm", ok: true, detail: `${PKG_NAME}@${spec} is not on npm yet` }], ok: true, alreadyCurrent: true, restart: [] };
|
|
98
|
+
}
|
|
99
|
+
return stop("npm", `\`npm install -g ${PKG_NAME}@${spec}\` failed: ${e.message.split("\n")[0]}. ` +
|
|
96
100
|
`You may need to re-run it with the right permissions (or your Node version manager's global scope).`);
|
|
97
101
|
}
|
|
98
102
|
let wired = [];
|