@join-pantheon/cli 0.29.1 → 0.31.0
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 +142 -40
- package/bin/pantheon.js +12 -0
- package/dist/accounts.js +151 -26
- package/dist/agents.js +224 -17
- package/dist/claude-confine.js +299 -0
- package/dist/cli.js +1116 -137
- package/dist/client.js +294 -10
- package/dist/command-state.js +102 -0
- package/dist/config.js +287 -14
- package/dist/continued.js +42 -10
- package/dist/doctor.js +191 -23
- package/dist/file-lock.js +197 -0
- package/dist/grants.js +1 -1
- package/dist/history-view.js +2 -1
- package/dist/home-guard.js +565 -0
- package/dist/hooks-install.js +2 -1
- package/dist/identity.js +101 -1
- package/dist/install.js +76 -26
- package/dist/kiro-files.js +2 -2
- package/dist/ledger.js +111 -28
- package/dist/machine.js +494 -32
- package/dist/mcp-supervisor.js +29 -7
- package/dist/mcp-windows.js +47 -25
- package/dist/mcp.js +99 -36
- package/dist/network.js +39 -8
- package/dist/nonces.js +130 -0
- package/dist/notify.js +26 -9
- package/dist/pending.js +121 -22
- package/dist/persona.js +21 -0
- package/dist/pins.js +161 -0
- package/dist/profile.js +22 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +173 -24
- package/dist/reads.js +37 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1341 -207
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +190 -25
- package/dist/run-failure.js +99 -3
- package/dist/secret-guard.js +141 -0
- package/dist/service.js +91 -35
- package/dist/setup-guide.js +91 -31
- package/dist/spend.js +6 -2
- package/dist/store.js +94 -10
- package/dist/text-file.js +74 -0
- package/dist/tray.js +24 -3
- package/dist/untrusted.js +24 -5
- package/dist/update-check.js +2 -0
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/which.js +69 -12
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +231 -7
- package/dist/worker.js +1717 -130
- package/helper/macos/menubar.swift +63 -2
- package/package.json +2 -2
package/dist/setup-guide.js
CHANGED
|
@@ -3,10 +3,11 @@ import { approvalSummary, installForUser } from "./install.js";
|
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { applyProfileInit, loadProfile, profilePath, saveProfile } from "./profile.js";
|
|
6
|
+
import { applyProfileInit, loadProfile, profilePath, saveProfile, wakeMode } from "./profile.js";
|
|
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 (
|
|
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 = [
|
|
@@ -32,7 +33,7 @@ export const ALERT_OPTIONS = [
|
|
|
32
33
|
export function guideDefaults(profile, cfg) {
|
|
33
34
|
const n = profile.notifications ?? { decisions: true, deliveries: false, worker_done: false };
|
|
34
35
|
return {
|
|
35
|
-
wake: profile
|
|
36
|
+
wake: wakeMode(profile),
|
|
36
37
|
commands: profile.agent.allow_bash ?? false,
|
|
37
38
|
approval: profile.agent.approval ?? "approve",
|
|
38
39
|
name: profile.human.display_name || cfg.display || cfg.member,
|
|
@@ -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,10 +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
|
-
"
|
|
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.",
|
|
74
77
|
"",
|
|
75
78
|
"1. While they're away, may teammates hand work to this machine? (their agent runs here on its own,",
|
|
76
79
|
" in a separate copy of the repo, and delivers on a branch for review)",
|
|
@@ -98,7 +101,7 @@ export function questionnaire() {
|
|
|
98
101
|
" These are switches: pass --approve-all or --ask-before-sending with no value. Either way the install",
|
|
99
102
|
" output prints the exact line written to ~/.codex/config.toml, and pantheon leave removes it.",
|
|
100
103
|
"",
|
|
101
|
-
"Example: pantheon setup --yes --wake
|
|
104
|
+
"Example: pantheon setup --yes --wake approved --commands yes --name \"Praneet Sinha\" --title Founder \\",
|
|
102
105
|
" --decides deployment --alerts decisions --playbook yes --base main",
|
|
103
106
|
].join("\n");
|
|
104
107
|
}
|
|
@@ -120,6 +123,9 @@ export function parseSetupFlags(argv) {
|
|
|
120
123
|
const commands = yn("commands");
|
|
121
124
|
if (commands !== undefined)
|
|
122
125
|
preset.commands = commands;
|
|
126
|
+
const unsandboxed = yn("unsandboxed");
|
|
127
|
+
if (unsandboxed !== undefined)
|
|
128
|
+
preset.unsandboxed = unsandboxed;
|
|
123
129
|
const name = val("name");
|
|
124
130
|
if (name !== undefined)
|
|
125
131
|
preset.name = name.trim();
|
|
@@ -158,6 +164,7 @@ export function parseSetupFlags(argv) {
|
|
|
158
164
|
}
|
|
159
165
|
export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
160
166
|
const optOut = ctx.optOut ?? {};
|
|
167
|
+
const serves = ctx.project ? { path: ctx.project, repos: true } : ctx.serves ?? null;
|
|
161
168
|
const os = deps.platform ?? process.platform;
|
|
162
169
|
const mac = os === "darwin";
|
|
163
170
|
const install = deps.installService ?? realInstall;
|
|
@@ -166,29 +173,27 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
166
173
|
const file = deps.profileFile ?? profilePath();
|
|
167
174
|
const profile = loadProfile(ctx.cfg, file);
|
|
168
175
|
const d = { ...guideDefaults(profile, ctx.cfg), ...preset };
|
|
176
|
+
if (profile.agent.allow_bash === undefined && preset.commands === undefined && serves?.repos)
|
|
177
|
+
d.commands = true;
|
|
169
178
|
const a = { ...d };
|
|
170
179
|
if (a.wake === "no") {
|
|
171
180
|
a.commands = false;
|
|
172
181
|
a.persona = false;
|
|
173
182
|
}
|
|
174
183
|
const applied = [], warnings = [];
|
|
184
|
+
const awayAgents = deps.unsandboxedAgents?.() ?? [];
|
|
175
185
|
if (io) {
|
|
176
|
-
io.say("
|
|
186
|
+
io.say("A few questions, then one screen of defaults. Enter keeps the suggested answer.\n");
|
|
177
187
|
a.name = (await io.ask("Your name, as teammates will see it", d.name)).trim() || d.name;
|
|
178
188
|
const known = TITLE_OPTIONS.findIndex((t) => t.toLowerCase() === d.title.toLowerCase());
|
|
179
|
-
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);
|
|
180
190
|
a.title = t === TITLE_OPTIONS.length - 1 ? (await io.ask("Your role", d.title)).trim() || d.title || "Member" : TITLE_OPTIONS[t];
|
|
181
191
|
const askCommands = async () => {
|
|
182
192
|
const undecided = profile.agent.allow_bash === undefined && preset.commands === undefined;
|
|
183
|
-
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)",
|
|
184
194
|
"No — edit files and answer only; it cannot run tests, commit, or push"], undecided || a.commands ? 0 : 1);
|
|
185
195
|
a.commands = c === 0;
|
|
186
196
|
};
|
|
187
|
-
let commandsAsked = false;
|
|
188
|
-
if (a.wake !== "no" && ctx.project) {
|
|
189
|
-
await askCommands();
|
|
190
|
-
commandsAsked = true;
|
|
191
|
-
}
|
|
192
197
|
const detected = ctx.project && deps.setRepoBase ? detectBaseBranch(ctx.project) : null;
|
|
193
198
|
const suggestedBase = detected ? (preset.base ?? detected) : undefined;
|
|
194
199
|
if (a.wake !== "no" && ctx.project)
|
|
@@ -196,9 +201,18 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
196
201
|
const shortAlert = (id) => ALERT_OPTIONS.find((o) => o.id === id).label.split(" — ")[0].replace(/ \(recommended\)/, "");
|
|
197
202
|
io.say("\nThe rest has defaults. Change them now, or any time with `pantheon setup`:");
|
|
198
203
|
io.say(` • Wake: ${a.wake === "no" ? "off — this machine only works while you are at it"
|
|
199
|
-
: !
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
}
|
|
202
216
|
io.say(` • Alerts: ${shortAlert(a.notifications).toLowerCase()}`);
|
|
203
217
|
io.say(` • Approval: ${a.approval === "approve"
|
|
204
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)"
|
|
@@ -220,9 +234,10 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
220
234
|
a.commands = false;
|
|
221
235
|
a.persona = false;
|
|
222
236
|
}
|
|
223
|
-
else if (
|
|
237
|
+
else if (serves?.repos) {
|
|
224
238
|
await askCommands();
|
|
225
|
-
|
|
239
|
+
if (ctx.project)
|
|
240
|
+
a.persona = preset.persona ?? true;
|
|
226
241
|
}
|
|
227
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));
|
|
228
243
|
a.decides = [...new Set(picks.map((i) => DECISION_AREAS[i].id))];
|
|
@@ -305,22 +320,50 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
305
320
|
else
|
|
306
321
|
applied.push("worker stays off");
|
|
307
322
|
}
|
|
308
|
-
else if (
|
|
323
|
+
else if (serves) {
|
|
309
324
|
try {
|
|
310
|
-
install("worker",
|
|
311
|
-
applied.push(
|
|
312
|
-
? "worker on — a
|
|
313
|
-
:
|
|
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)");
|
|
314
331
|
}
|
|
315
332
|
catch (e) {
|
|
316
333
|
warnings.push(`the worker service did not install: ${e.message}`);
|
|
317
334
|
}
|
|
318
335
|
applied.push(a.wake === "anyone"
|
|
319
|
-
? "wake permission: invites you send
|
|
320
|
-
: "wake permission: only people you
|
|
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>)");
|
|
321
338
|
}
|
|
322
339
|
else {
|
|
323
|
-
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`);
|
|
324
367
|
}
|
|
325
368
|
const waitIn = mac ? "pantheon status, approvals, and the menu bar" : "pantheon status and pantheon approvals";
|
|
326
369
|
const chosenAlerts = ALERT_OPTIONS.find((o) => o.id === a.notifications).label.split(" — ")[0].replace(/ \(recommended\)/, "");
|
|
@@ -403,8 +446,25 @@ ${never.map((n) => `- ${n}`).join("\n")}
|
|
|
403
446
|
When in doubt, ask the teammate who woke you rather than ${who}; if only ${who} can answer, ESCALATE and finish.
|
|
404
447
|
`;
|
|
405
448
|
}
|
|
449
|
+
export function questionOrNull(rl, q) {
|
|
450
|
+
return new Promise((res) => {
|
|
451
|
+
let done = false;
|
|
452
|
+
const onClose = () => { if (!done) {
|
|
453
|
+
done = true;
|
|
454
|
+
res(null);
|
|
455
|
+
} };
|
|
456
|
+
rl.once?.("close", onClose);
|
|
457
|
+
try {
|
|
458
|
+
rl.question(q, (a) => { if (done)
|
|
459
|
+
return; done = true; rl.off?.("close", onClose); res(a.trim()); });
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
onClose();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
406
466
|
export function terminalIO(rl, out) {
|
|
407
|
-
const ask = (q) =>
|
|
467
|
+
const ask = async (q) => (await questionOrNull(rl, q)) ?? "";
|
|
408
468
|
return {
|
|
409
469
|
say: out,
|
|
410
470
|
async ask(question, def) {
|
package/dist/spend.js
CHANGED
|
@@ -31,9 +31,13 @@ export function since(ms, home) {
|
|
|
31
31
|
byPeer[r.triggeredBy] = (byPeer[r.triggeredBy] ?? 0) + 1;
|
|
32
32
|
return { runs: runs.length, failed: runs.filter((r) => !r.ok).length, byPeer };
|
|
33
33
|
}
|
|
34
|
-
export function recentRuns(ms, home, limit = 12) {
|
|
34
|
+
export function recentRuns(ms, home, limit = 12, only) {
|
|
35
35
|
const from = Date.now() - ms;
|
|
36
|
-
|
|
36
|
+
const teams = (only?.teams ?? []).filter((t) => !!t);
|
|
37
|
+
return read(home).runs
|
|
38
|
+
.filter((r) => r.at > from)
|
|
39
|
+
.filter((r) => !only || (!!r.space && teams.includes(r.space) && (!r.member || r.member === only.member)))
|
|
40
|
+
.sort((a, b) => b.at - a.at).slice(0, limit);
|
|
37
41
|
}
|
|
38
42
|
export function allowed(caps, home) {
|
|
39
43
|
const hour = since(3_600_000, home).runs;
|
package/dist/store.js
CHANGED
|
@@ -1,22 +1,86 @@
|
|
|
1
|
-
import { readFileSync,
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, renameSync, openSync, writeSync, fsyncSync, closeSync, chmodSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { emptyRefusals } from "./admin-stats.js";
|
|
4
|
+
export function ensurePrivateDir(dir) {
|
|
5
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
6
|
+
try {
|
|
7
|
+
chmodSync(dir, 0o700);
|
|
8
|
+
}
|
|
9
|
+
catch { }
|
|
10
|
+
}
|
|
11
|
+
export function writeFileAtomic(file, data, mode = 0o600) {
|
|
12
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
13
|
+
const tmp = `${file}.tmp`;
|
|
14
|
+
const fd = openSync(tmp, "w", mode);
|
|
15
|
+
try {
|
|
16
|
+
writeSync(fd, typeof data === "string" ? Buffer.from(data) : data);
|
|
17
|
+
fsyncSync(fd);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
closeSync(fd);
|
|
21
|
+
}
|
|
22
|
+
renameRetrying(tmp, file);
|
|
23
|
+
fsyncDir(dirname(file));
|
|
24
|
+
}
|
|
25
|
+
const sleepSync = (ms) => { try {
|
|
26
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
27
|
+
}
|
|
28
|
+
catch { } };
|
|
29
|
+
export function renameRetrying(from, to, o = {}) {
|
|
30
|
+
const platform = o.platform ?? process.platform;
|
|
31
|
+
const rename = o.rename ?? renameSync;
|
|
32
|
+
for (let i = 0;; i++) {
|
|
33
|
+
try {
|
|
34
|
+
rename(from, to);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (platform !== "win32" || i >= 20 || !["EPERM", "EACCES", "EBUSY"].includes(e.code ?? ""))
|
|
39
|
+
throw e;
|
|
40
|
+
sleepSync(o.waitMs ?? 10);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function fsyncDir(dir) {
|
|
45
|
+
let fd = null;
|
|
46
|
+
try {
|
|
47
|
+
fd = openSync(dir, "r");
|
|
48
|
+
fsyncSync(fd);
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
finally {
|
|
52
|
+
if (fd !== null)
|
|
53
|
+
try {
|
|
54
|
+
closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
4
59
|
export class StateStore {
|
|
5
60
|
file;
|
|
6
61
|
timer = null;
|
|
7
62
|
dirty = false;
|
|
8
63
|
refusals = emptyRefusals();
|
|
64
|
+
headOf;
|
|
65
|
+
rebuilt = null;
|
|
66
|
+
extra = {};
|
|
9
67
|
constructor(dir, space) {
|
|
10
68
|
this.file = join(dir, `${space}.state.json`);
|
|
11
69
|
}
|
|
12
|
-
load() {
|
|
70
|
+
load(opts = {}) {
|
|
13
71
|
const goals = new Map();
|
|
14
72
|
const obligations = new Map();
|
|
15
73
|
const profiles = new Map();
|
|
16
|
-
if (!existsSync(this.file))
|
|
17
|
-
return { goals, obligations, profiles };
|
|
74
|
+
if (!existsSync(this.file)) {
|
|
75
|
+
return { goals, obligations, profiles, unclean: opts.expectHistory ? "the state snapshot is missing but the ledger has history" : null };
|
|
76
|
+
}
|
|
18
77
|
try {
|
|
19
78
|
const snap = JSON.parse(readFileSync(this.file, "utf8"));
|
|
79
|
+
if (!snap || typeof snap !== "object" || Array.isArray(snap)
|
|
80
|
+
|| (snap.goals !== undefined && !Array.isArray(snap.goals))
|
|
81
|
+
|| (snap.obligations !== undefined && !Array.isArray(snap.obligations))) {
|
|
82
|
+
throw new Error("not a state snapshot");
|
|
83
|
+
}
|
|
20
84
|
for (const g of snap.goals ?? [])
|
|
21
85
|
goals.set(g.id, g);
|
|
22
86
|
for (const o of snap.obligations ?? []) {
|
|
@@ -24,12 +88,32 @@ export class StateStore {
|
|
|
24
88
|
}
|
|
25
89
|
for (const p of snap.profiles ?? [])
|
|
26
90
|
profiles.set(p.member, p);
|
|
91
|
+
const known = new Set(["v", "goals", "obligations", "profiles", "refusals", "ledger_head", "rebuilt", "saved_at"]);
|
|
92
|
+
this.extra = {};
|
|
93
|
+
for (const [k, v] of Object.entries(snap))
|
|
94
|
+
if (!known.has(k))
|
|
95
|
+
this.extra[k] = v;
|
|
27
96
|
if (snap.refusals)
|
|
28
97
|
this.refusals = { total: snap.refusals.total ?? 0, by_code: snap.refusals.by_code ?? {}, recent: snap.refusals.recent ?? [], stamps: snap.refusals.stamps ?? [] };
|
|
98
|
+
return {
|
|
99
|
+
goals, obligations, profiles, unclean: null,
|
|
100
|
+
...(typeof snap.ledger_head === "number" && Number.isFinite(snap.ledger_head) ? { ledgerHead: snap.ledger_head } : {}),
|
|
101
|
+
...(typeof snap.rebuilt === "string" && snap.rebuilt ? { rebuilt: snap.rebuilt } : {}),
|
|
102
|
+
};
|
|
29
103
|
}
|
|
30
|
-
catch {
|
|
104
|
+
catch (e) {
|
|
105
|
+
const why = `the state snapshot could not be read (${e.message})`;
|
|
106
|
+
let quarantined;
|
|
107
|
+
if (opts.quarantine) {
|
|
108
|
+
const aside = `${this.file}.corrupt-${Date.now()}`;
|
|
109
|
+
try {
|
|
110
|
+
renameSync(this.file, aside);
|
|
111
|
+
quarantined = aside;
|
|
112
|
+
}
|
|
113
|
+
catch { }
|
|
114
|
+
}
|
|
115
|
+
return { goals: new Map(), obligations: new Map(), profiles: new Map(), unclean: why, ...(quarantined ? { quarantined } : {}) };
|
|
31
116
|
}
|
|
32
|
-
return { goals, obligations, profiles };
|
|
33
117
|
}
|
|
34
118
|
save(goals, obligations, profiles) {
|
|
35
119
|
this.dirty = true;
|
|
@@ -52,17 +136,17 @@ export class StateStore {
|
|
|
52
136
|
onError;
|
|
53
137
|
flush(goals, obligations, profiles) {
|
|
54
138
|
const snap = {
|
|
139
|
+
...this.extra,
|
|
55
140
|
v: 2,
|
|
56
141
|
goals: [...goals.values()],
|
|
57
142
|
obligations: [...obligations.values()],
|
|
58
143
|
profiles: [...profiles.values()],
|
|
59
144
|
...(this.refusals.total ? { refusals: this.refusals } : {}),
|
|
145
|
+
...(this.headOf ? { ledger_head: this.headOf() } : {}),
|
|
146
|
+
...(this.rebuilt ? { rebuilt: this.rebuilt } : {}),
|
|
60
147
|
saved_at: Date.now(),
|
|
61
148
|
};
|
|
62
|
-
|
|
63
|
-
const tmp = `${this.file}.tmp`;
|
|
64
|
-
writeFileSync(tmp, JSON.stringify(snap, null, 2) + "\n", { mode: 0o600 });
|
|
65
|
-
renameSync(tmp, this.file);
|
|
149
|
+
writeFileAtomic(this.file, JSON.stringify(snap, null, 2) + "\n");
|
|
66
150
|
}
|
|
67
151
|
close(goals, obligations, profiles) {
|
|
68
152
|
if (this.timer) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const stripBom = (text) => (text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
|
|
2
|
+
export const parseJsonText = (text) => JSON.parse(stripBom(text));
|
|
3
|
+
export function stripJsonc(text) {
|
|
4
|
+
let out = "";
|
|
5
|
+
let i = 0;
|
|
6
|
+
const n = text.length;
|
|
7
|
+
while (i < n) {
|
|
8
|
+
const ch = text[i];
|
|
9
|
+
if (ch === '"') {
|
|
10
|
+
let j = i + 1;
|
|
11
|
+
while (j < n && text[j] !== '"')
|
|
12
|
+
j += text[j] === "\\" ? 2 : 1;
|
|
13
|
+
out += text.slice(i, j + 1);
|
|
14
|
+
i = j + 1;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
18
|
+
while (i < n && text[i] !== "\n")
|
|
19
|
+
i++;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
23
|
+
const end = text.indexOf("*/", i + 2);
|
|
24
|
+
i = end < 0 ? n : end + 2;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (ch === ",") {
|
|
28
|
+
let j = i + 1;
|
|
29
|
+
for (;;) {
|
|
30
|
+
while (j < n && /\s/.test(text[j]))
|
|
31
|
+
j++;
|
|
32
|
+
if (text[j] === "/" && text[j + 1] === "/") {
|
|
33
|
+
while (j < n && text[j] !== "\n")
|
|
34
|
+
j++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (text[j] === "/" && text[j + 1] === "*") {
|
|
38
|
+
const e = text.indexOf("*/", j + 2);
|
|
39
|
+
j = e < 0 ? n : e + 2;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
if (text[j] === "}" || text[j] === "]") {
|
|
45
|
+
i++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
out += ch;
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
export function isJsonc(text) {
|
|
55
|
+
const t = stripBom(text);
|
|
56
|
+
try {
|
|
57
|
+
JSON.parse(t);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
try {
|
|
62
|
+
JSON.parse(stripJsonc(t));
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function eolOf(text) {
|
|
70
|
+
const i = text.indexOf("\n");
|
|
71
|
+
return i > 0 && text[i - 1] === "\r" ? "\r\n" : "\n";
|
|
72
|
+
}
|
|
73
|
+
export const withEol = (text, eol) => eol === "\n" ? text : text.replace(/\r?\n/g, "\r\n");
|
|
74
|
+
//# sourceMappingURL=text-file.js.map
|
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";
|
|
@@ -11,6 +12,7 @@ import { requireDeveloperTools } from "./notifier.js";
|
|
|
11
12
|
import { plistPath, serviceInstalled, whichPantheon } from "./service.js";
|
|
12
13
|
import { VERSION } from "./version.js";
|
|
13
14
|
import { LIMITS } from "./protocol.js";
|
|
15
|
+
import { signedRead } from "./reads.js";
|
|
14
16
|
import { allDecisionsPrompt, decisionPrompt, decisionTitle, owedPrompt, stripCeremony, waitingPrompt, workerSummary } from "./decision-ui.js";
|
|
15
17
|
import { loadProfile, SURFACE_BUNDLE_IDS } from "./profile.js";
|
|
16
18
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -50,6 +52,16 @@ export function writeTrayCommands(home = CONFIG_DIR) {
|
|
|
50
52
|
}
|
|
51
53
|
return out;
|
|
52
54
|
}
|
|
55
|
+
export function localChoices(ref) {
|
|
56
|
+
if (!ref)
|
|
57
|
+
return null;
|
|
58
|
+
const pol = loadPolicy();
|
|
59
|
+
if (pol.wakeAsks?.[ref])
|
|
60
|
+
return [{ label: "Always", answer: "allow always" }, { label: "Just this once", answer: "allow once" }, { label: "No", answer: "deny" }];
|
|
61
|
+
if (Object.values(pol.asks).some((a) => a?.act === ref))
|
|
62
|
+
return [{ label: "Allow always", answer: "allow always" }, { label: "Allow once", answer: "allow once" }, { label: "Don't allow", answer: "deny" }];
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
53
65
|
function workerState(home = CONFIG_DIR) {
|
|
54
66
|
try {
|
|
55
67
|
const raw = JSON.parse(readFileSync(join(home, "worker-state.json"), "utf8"));
|
|
@@ -75,6 +87,15 @@ async function getJson(url) {
|
|
|
75
87
|
clearTimeout(t);
|
|
76
88
|
}
|
|
77
89
|
}
|
|
90
|
+
async function signedObligations(cfg) {
|
|
91
|
+
try {
|
|
92
|
+
const res = await signedRead(cfg, "/obligations", {}, { timeoutMs: LIMITS.pendingTimeoutMs });
|
|
93
|
+
return res.ok ? await res.json() : null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
78
99
|
export async function trayStatus(cfg = load(), f = loadAll()) {
|
|
79
100
|
const ms = Object.values(f?.spaces ?? {});
|
|
80
101
|
if (f && ms.length > 1) {
|
|
@@ -105,10 +126,9 @@ async function trayStatusOne(cfg) {
|
|
|
105
126
|
if (!cfg)
|
|
106
127
|
return { configured: false, version: VERSION };
|
|
107
128
|
const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
108
|
-
const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`;
|
|
109
129
|
const [health, obligations] = await Promise.all([
|
|
110
130
|
getJson(`${base}/health?space=${encodeURIComponent(cfg.space)}`),
|
|
111
|
-
|
|
131
|
+
signedObligations(cfg),
|
|
112
132
|
]);
|
|
113
133
|
const space = health
|
|
114
134
|
?.spaces?.find((s) => s.name === cfg.space);
|
|
@@ -131,7 +151,8 @@ async function trayStatusOne(cfg) {
|
|
|
131
151
|
const items = [
|
|
132
152
|
...mineHuman.map((o) => {
|
|
133
153
|
const from = senderOf(o);
|
|
134
|
-
|
|
154
|
+
const choices = localChoices(o.ref);
|
|
155
|
+
return { ...row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id, decisionTitle(o.body))), decider: cfg.member, ...(choices ? { choices } : {}) };
|
|
135
156
|
}),
|
|
136
157
|
...owed.map((o) => row(o, "owed", "you", owedPrompt(decisionTitle(o.body), o.goal))),
|
|
137
158
|
...waiting.map((o) => {
|
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/update-check.js
CHANGED
|
@@ -20,6 +20,8 @@ export function updateStatus(deps) {
|
|
|
20
20
|
if (cache && deps.now() - cache.checkedAt < deps.throttleMs) {
|
|
21
21
|
return { behind: cache.behind, refreshed: false, latest: cache.latest };
|
|
22
22
|
}
|
|
23
|
+
if (process.env["PANTHEON_NO_UPDATE_CHECK"])
|
|
24
|
+
return { behind: cache?.behind ?? 0, refreshed: false, latest: cache?.latest };
|
|
23
25
|
if (deps.isGit()) {
|
|
24
26
|
try {
|
|
25
27
|
const behind = deps.fetchBehind();
|