@join-pantheon/cli 0.30.0 → 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.
@@ -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
@@ -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
- if (existing?.revoked_at && inv.created_at <= existing.revoked_at) {
234
- return { ok: false, why: `"${member}" was removed from this team${existing.revoked_by ? ` by ${existing.revoked_by}` : ""}, so an invite from before that cannot bring them back — ask the founder for a new one` };
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();
@@ -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
- return { disabled, loaded: pr.ok, running: pr.ok && /^\s*state = running/m.test(pr.out) };
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) {
@@ -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; you can revoke a person any time." },
23
- { id: "approved", label: "Only people I approve one by one (the default) — invites do not grant it; I run `pantheon wake allow <name>` per person, on this machine." },
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 every time. Ask 2 whenever there is a repo (it is the one default people are",
71
- "surprised by). The rest have defaults you may take and say out loud:",
72
- " --wake approved --decides none --alerts decisions --playbook yes --base <what origin calls its default> --approve-all",
73
- "--wake approved (the default) means nobody wakes this machine until they approve a person with `pantheon wake allow <name>`.",
74
- "Never pick --wake anyone for them: it is their call, and only if they say so.",
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("Three questions, then one screen of defaults. Enter keeps the suggested answer.\n");
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 execute commands?", ["Yes — run tests, commit, and push work for review (recommended)",
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
- : !ctx.project ? "on once this machine has a repo (run pantheon setup from your project's repo)"
202
- : a.wake === "anyone" ? "teammates may hand work to this machine while you're away; invites you send grant it, and you can revoke anyone (pantheon admit <name> --revoke)"
203
- : "only people you approve by hand, on this machine (pantheon wake allow <name>)"}`);
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 (ctx.project && !commandsAsked) {
237
+ else if (serves?.repos) {
226
238
  await askCommands();
227
- a.persona = preset.persona ?? true;
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 (ctx.project) {
323
+ else if (serves) {
311
324
  try {
312
- install("worker", ctx.project, a.commands ? ["--allow-bash"] : []);
313
- applied.push(a.commands
314
- ? "worker on — a woken run may run tests and commit and push to its own branch"
315
- : "worker on — a woken run edits files and answers; it cannot commit or push (deliveries will need you)");
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 grant it (revoke one person: pantheon admit <name> --revoke)"
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("worker: run this again from your project's repo to switch it on there");
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,16 @@ export function writeTrayCommands(home = CONFIG_DIR) {
51
52
  }
52
53
  return out;
53
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
+ }
54
65
  function workerState(home = CONFIG_DIR) {
55
66
  try {
56
67
  const raw = JSON.parse(readFileSync(join(home, "worker-state.json"), "utf8"));
@@ -140,7 +151,8 @@ async function trayStatusOne(cfg) {
140
151
  const items = [
141
152
  ...mineHuman.map((o) => {
142
153
  const from = senderOf(o);
143
- return { ...row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id, decisionTitle(o.body))), decider: cfg.member };
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 } : {}) };
144
156
  }),
145
157
  ...owed.map((o) => row(o, "owed", "you", owedPrompt(decisionTitle(o.body), o.goal))),
146
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: /\b(password|passphrase|secret\s*key|api[_ -]?key|private[_ -]?key|credential)s?\b/i },
7
- { kind: "credential", why: "names an env var that usually holds a secret", re: /\b[A-Z_]*(TOKEN|SECRET|PASSWORD|API_KEY)\b/ },
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\/|credentials?\b|\.pem\b)/i },
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|authenticate|create an account|join .{0,20}hub)\b/i },
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}@latest`, "--no-audit", "--no-fund"], { stdio: ["ignore", "pipe", "pipe"] }),
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}@latest` });
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
- return stop("npm", `\`npm install -g ${PKG_NAME}@latest\` failed: ${e.message.split("\n")[0]}. ` +
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 = [];
@@ -0,0 +1,102 @@
1
+ import { actPayload, canonicalJson, canonicalKey, fingerprint, goalPayload, verifyBytes, verifyDelegate } from "./identity.js";
2
+ export const RELAY_META_KEYS = new Set(["from_repo", "authority", "unverified_domains", "defers_to", "affects"]);
3
+ export function relayComposed(rec) {
4
+ if (rec.from === "relay" || rec.from === "pantheon")
5
+ return true;
6
+ if (rec.act === "JOIN" || rec.act === "LEAVE")
7
+ return true;
8
+ const m = rec.meta ?? {};
9
+ if (rec.act === "NOTE" && ("goal_members" in m || "organization_profile" in m || "notice" in m))
10
+ return true;
11
+ if (rec.act === "NOTE" && typeof rec.body === "string" && rec.body.startsWith("repo registered with this space: "))
12
+ return true;
13
+ return false;
14
+ }
15
+ const same = (a, b) => canonicalJson(a ?? null) === canonicalJson(b ?? null);
16
+ const teamList = (t) => [...new Set((typeof t === "string" ? [t] : t).filter((x) => !!x))];
17
+ export function verifyAct(rec, teams, pinnedFp, memberKey, at = Date.now()) {
18
+ const sig = rec.sig;
19
+ if (!sig)
20
+ return relayComposed(rec) ? { trust: "relay", why: "recorded by the relay" } : { trust: "unsigned", why: `not signed — sent by a Pantheon before 0.31, or the signature was dropped on the way` };
21
+ if (sig.v !== 1 || typeof sig.s !== "string" || typeof sig.pub !== "string")
22
+ return { trust: "invalid", why: "a signature in a form this version does not know" };
23
+ if (!canonicalKey(sig.pub))
24
+ return { trust: "invalid", why: "signed with a key that is not a plain ed25519 key" };
25
+ if (typeof sig.ts !== "number" || sig.ts > Date.now() + 86_400_000)
26
+ return { trust: "invalid", why: "signed with a date more than a day ahead" };
27
+ const senderMeta = (sig.meta ?? {});
28
+ const meta = (rec.meta ?? {});
29
+ for (const [k, v] of Object.entries(senderMeta))
30
+ if (!same(meta[k], v))
31
+ return { trust: "invalid", why: `meta.${k} is not what the sender wrote` };
32
+ for (const k of Object.keys(meta))
33
+ if (!(k in senderMeta) && !RELAY_META_KEYS.has(k))
34
+ return { trust: "invalid", why: `meta.${k} was added on the way` };
35
+ if (typeof meta.from_repo === "string" && meta.from_repo !== (sig.repo ?? null))
36
+ return { trust: "invalid", why: "from_repo is not the repo the sender signed" };
37
+ const list = teamList(teams);
38
+ const team = list.find((t) => verifyBytes(sig.pub, actPayload({
39
+ id: rec.id, team: t, member: rec.from, keyRole: sig.role, nonce: sig.nonce, ts: sig.ts,
40
+ act: rec.act, goal: rec.goal ?? null, ref: rec.ref ?? null, to: rec.to ?? null,
41
+ defers_to: typeof meta.defers_to === "string" ? meta.defers_to : null,
42
+ affects: Array.isArray(meta.affects) ? meta.affects : null,
43
+ repo: sig.repo ?? null,
44
+ summary: rec.summary ?? null, body: rec.body, meta: senderMeta,
45
+ }), sig.s));
46
+ if (!team)
47
+ return { trust: "invalid", why: "the signature does not match what arrived — altered on the way, or not written by its sender" };
48
+ return signer(sig, team, rec.from, pinnedFp, memberKey, Math.max(sig.ts, rec.ts || 0), "act");
49
+ }
50
+ function signer(sig, team, member, pinnedFp, memberKey, at, what) {
51
+ if (!pinnedFp)
52
+ return { trust: "unknown-key", why: `no key for ${member} is known on this machine yet` };
53
+ const pinnedKey = memberKey && canonicalKey(memberKey) && fingerprint(memberKey) === pinnedFp ? memberKey : undefined;
54
+ if (sig.role === "member") {
55
+ const same = pinnedKey ? sig.pub === pinnedKey : fingerprint(sig.pub) === pinnedFp;
56
+ if (!same)
57
+ return { trust: "key-changed", why: `signed by a key that is not the one this machine knows for ${member} (${fingerprint(sig.pub)}; known: ${pinnedFp})` };
58
+ return { trust: "verified", why: `signed by ${member}` };
59
+ }
60
+ const cert = sig.cert;
61
+ if (!cert)
62
+ return { trust: "invalid", why: `a run's ${what} with no certificate` };
63
+ if (cert.pub !== sig.pub)
64
+ return { trust: "invalid", why: "the run key is not the one its certificate names" };
65
+ if (!pinnedKey)
66
+ return { trust: "unknown-key", why: `${member}'s own key is needed to check their run's certificate, and this machine does not have it yet` };
67
+ const memberKey2 = pinnedKey;
68
+ if (!verifyDelegate(memberKey2, team, member, cert, at)) {
69
+ return { trust: "invalid", why: `the run certificate does not verify for ${member} in this team, or it had expired` };
70
+ }
71
+ if (sig.ts > cert.expires + 10 * 60_000)
72
+ return { trust: "invalid", why: "signed after its run certificate expired" };
73
+ return { trust: "verified-run", why: `signed by ${member}'s unattended run` };
74
+ }
75
+ export function verifyGoal(g, teams, pinnedFp, memberKey, at = Date.now()) {
76
+ const sig = g.sig;
77
+ if (!sig)
78
+ return { trust: "unsigned", why: "the goal was opened by a Pantheon before 0.31, or its signature was dropped" };
79
+ if (!canonicalKey(sig.pub))
80
+ return { trust: "invalid", why: "signed with a key that is not a plain ed25519 key" };
81
+ if (typeof sig.ts !== "number" || sig.ts > Date.now() + 86_400_000)
82
+ return { trust: "invalid", why: "signed with a date more than a day ahead" };
83
+ const team = teamList(teams).find((t) => verifyBytes(sig.pub, goalPayload({
84
+ id: g.id, team: t, member: g.opened_by, keyRole: sig.role, nonce: sig.nonce, ts: sig.ts,
85
+ title: g.title, acceptance: g.acceptance, invite: sig.invite,
86
+ budget: sig.budget ?? null, deadline: sig.deadline ?? null, kind: sig.kind ?? null,
87
+ }), sig.s));
88
+ if (!team)
89
+ return { trust: "invalid", why: "the goal's title, acceptance or invitations are not what its opener wrote" };
90
+ at = Math.max(sig.ts, g.opened_at || 0);
91
+ return signer(sig, team, g.opened_by, pinnedFp, memberKey, at, "goal");
92
+ }
93
+ export const trusted = (t) => t === "verified" || t === "verified-run";
94
+ export function trustLabel(r, from) {
95
+ switch (r.trust) {
96
+ case "verified": return `signed by ${from}`;
97
+ case "verified-run": return `signed by ${from}'s unattended run`;
98
+ case "relay": return from === "relay" || from === "pantheon" ? "written by the relay" : `written by the relay, not signed by ${from} — NOT VERIFIED as theirs`;
99
+ default: return `NOT VERIFIED — ${r.why}`;
100
+ }
101
+ }
102
+ //# sourceMappingURL=verify-act.js.map