@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,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
|
package/dist/worker-fleet.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve, sep } from "node:path";
|
|
3
|
+
import { holdsRepo } from "./repo-find.js";
|
|
3
4
|
import { CONFIG_DIR, configFor, loadAll, repoInfo } from "./config.js";
|
|
4
|
-
import { Worker } from "./worker.js";
|
|
5
|
+
import { Worker, workspaceOf } from "./worker.js";
|
|
5
6
|
export class WorkerFleet {
|
|
6
7
|
opts;
|
|
8
|
+
dupSaid = new Set();
|
|
7
9
|
slots = new Map();
|
|
8
10
|
stopped = false;
|
|
9
11
|
constructor(opts) {
|
|
@@ -18,14 +20,23 @@ export class WorkerFleet {
|
|
|
18
20
|
const wanted = new Map();
|
|
19
21
|
if (f) {
|
|
20
22
|
const several = Object.keys(f.spaces ?? {}).length > 1;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const idCount = new Map();
|
|
24
|
+
for (const m of Object.values(f.spaces ?? {}))
|
|
25
|
+
idCount.set(m.id, (idCount.get(m.id) ?? 0) + 1);
|
|
26
|
+
for (const m of Object.values(f.spaces ?? {})) {
|
|
27
|
+
if ((idCount.get(m.id) ?? 0) > 1) {
|
|
28
|
+
if (!this.dupSaid.has(m.id)) {
|
|
29
|
+
this.dupSaid.add(m.id);
|
|
30
|
+
this.opts.log(`fleet: two entries in config.json claim team ${m.id} — serving neither until one is removed (pantheon spaces)`);
|
|
31
|
+
}
|
|
24
32
|
continue;
|
|
25
|
-
|
|
33
|
+
}
|
|
34
|
+
const ws = teamWorkspaces(f, m.id);
|
|
35
|
+
const home = ws.find((w) => w.kind === "repo") ?? ws[0];
|
|
36
|
+
if (!home)
|
|
26
37
|
continue;
|
|
27
|
-
const repo = repoInfo(
|
|
28
|
-
wanted.set(m.id, { path:
|
|
38
|
+
const repo = home.kind === "repo" ? repoInfo(home.path) : null;
|
|
39
|
+
wanted.set(m.id, { path: home.path, cfg: configFor(f, m, repo) });
|
|
29
40
|
void several;
|
|
30
41
|
}
|
|
31
42
|
for (const [id, w] of wanted) {
|
|
@@ -36,6 +47,7 @@ export class WorkerFleet {
|
|
|
36
47
|
const wopts = {
|
|
37
48
|
...rest,
|
|
38
49
|
project: w.path,
|
|
50
|
+
workspaces: () => teamWorkspaces((this.opts.readConfig ?? loadAll)(), id),
|
|
39
51
|
scratch: join(home, "worktrees", encodeURIComponent(id)),
|
|
40
52
|
home,
|
|
41
53
|
requirePersonaSpaces: several,
|
|
@@ -66,7 +78,7 @@ export class WorkerFleet {
|
|
|
66
78
|
for (const s of stopped)
|
|
67
79
|
this.opts.log(`fleet: worker for "${s}" stopped — binding gone`);
|
|
68
80
|
if (!this.slots.size && !started.length)
|
|
69
|
-
this.opts.log(`fleet:
|
|
81
|
+
this.opts.log(`fleet: nothing bound on this machine — bind a repo or a docs folder with \`pantheon use <team> --here\` from inside it`);
|
|
70
82
|
await new Promise((r) => setTimeout(r, poll));
|
|
71
83
|
if (this.opts.once && this.slots.size === 0)
|
|
72
84
|
break;
|
|
@@ -81,4 +93,13 @@ export class WorkerFleet {
|
|
|
81
93
|
s.worker.stop();
|
|
82
94
|
}
|
|
83
95
|
}
|
|
96
|
+
export function teamWorkspaces(f, spaceId) {
|
|
97
|
+
const bound = Object.entries(f?.bindings ?? {}).filter(([, b]) => b.space === spaceId && existsSync(b.path));
|
|
98
|
+
const repos = bound.filter(([k, b]) => !k.startsWith("d_") && existsSync(join(b.path, ".git"))).map(([, b]) => workspaceOf(b.path));
|
|
99
|
+
const all = Object.values(f?.bindings ?? {}).map((b) => resolve(b.path));
|
|
100
|
+
const folders = bound.filter(([k]) => k.startsWith("d_")).map(([, b]) => workspaceOf(b.path))
|
|
101
|
+
.filter((w) => w.kind === "folder" && !all.some((p) => p !== resolve(w.path) && p.startsWith(`${resolve(w.path)}${sep}`)) && !holdsRepo(w.path));
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
return [...repos, ...folders].filter((w) => (seen.has(w.path) ? false : (seen.add(w.path), true)));
|
|
104
|
+
}
|
|
84
105
|
//# sourceMappingURL=worker-fleet.js.map
|
package/dist/worker-policy.js
CHANGED
|
@@ -3,6 +3,35 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { CONFIG_PATH } from "./config.js";
|
|
4
4
|
import { trustWrite } from "./home-guard.js";
|
|
5
5
|
import { parseJsonText } from "./text-file.js";
|
|
6
|
+
export const WAKE_NO_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
7
|
+
export const wakeKey = (team, member) => `${team}|${member}`;
|
|
8
|
+
export function mayAskWake(p, teams, member, now = Date.now()) {
|
|
9
|
+
const ts = teams.filter((t) => !!t);
|
|
10
|
+
if (isApprovedWaker(p, ts, member))
|
|
11
|
+
return false;
|
|
12
|
+
if (ts.some((t) => (p.wakeNo?.[wakeKey(t, member)] ?? 0) > now - WAKE_NO_MS))
|
|
13
|
+
return false;
|
|
14
|
+
return !Object.values(p.wakeAsks ?? {}).some((a) => ts.includes(a.team) && a.member === member && now - a.at < ASK_EXPIRY_MS);
|
|
15
|
+
}
|
|
16
|
+
export function takeWakeOnce(p, teams, member) {
|
|
17
|
+
for (const t of teams) {
|
|
18
|
+
if (!t)
|
|
19
|
+
continue;
|
|
20
|
+
const k = wakeKey(t, member);
|
|
21
|
+
const n = p.wakeOnce?.[k] ?? 0;
|
|
22
|
+
if (n > 0) {
|
|
23
|
+
if (n > 1)
|
|
24
|
+
p.wakeOnce[k] = n - 1;
|
|
25
|
+
else
|
|
26
|
+
delete p.wakeOnce[k];
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
export function hasWakeOnce(p, teams, member) {
|
|
33
|
+
return teams.some((t) => !!t && (p.wakeOnce?.[wakeKey(t, member)] ?? 0) > 0);
|
|
34
|
+
}
|
|
6
35
|
export const ASK_COOLDOWN_MS = 24 * 60 * 60 * 1_000;
|
|
7
36
|
export const ASK_EXPIRY_MS = 14 * 24 * 60 * 60 * 1_000;
|
|
8
37
|
export function policyHome(home) {
|
|
@@ -35,6 +64,10 @@ export function loadPolicy(home) {
|
|
|
35
64
|
...(wakers ? { wakers } : {}),
|
|
36
65
|
...(isRecord(raw.wakersCarried) ? { wakersCarried: raw.wakersCarried } : {}),
|
|
37
66
|
...(isRecord(raw.wakersDeferred) ? { wakersDeferred: raw.wakersDeferred } : {}),
|
|
67
|
+
...(isRecord(raw.wakeAsks) ? { wakeAsks: Object.fromEntries(Object.entries(raw.wakeAsks).filter(([, v]) => isRecord(v) && typeof v.team === "string" && typeof v.member === "string" && typeof v.at === "number")) } : {}),
|
|
68
|
+
...(isRecord(raw.wakeOnce) ? { wakeOnce: Object.fromEntries(Object.entries(raw.wakeOnce).filter(([, v]) => typeof v === "number" && v > 0 && v < 100)) } : {}),
|
|
69
|
+
...(isRecord(raw.wakeNo) ? { wakeNo: Object.fromEntries(Object.entries(raw.wakeNo).filter(([, v]) => typeof v === "number")) } : {}),
|
|
70
|
+
...(isRecord(raw.wakeTeamFlag) ? { wakeTeamFlag: Object.fromEntries(Object.entries(raw.wakeTeamFlag).filter(([, v]) => typeof v === "number")) } : {}),
|
|
38
71
|
};
|
|
39
72
|
}
|
|
40
73
|
catch {
|
|
@@ -117,14 +150,40 @@ export function setLocalWaker(teams, member, approved, home, env = process.env)
|
|
|
117
150
|
savePolicy(p, home);
|
|
118
151
|
return { ok: true, changed: true };
|
|
119
152
|
}
|
|
153
|
+
export function markTeamFlagOnYes(team, member, home, env = process.env) {
|
|
154
|
+
if (inWokenRun(env))
|
|
155
|
+
return;
|
|
156
|
+
const p = loadPolicy(home);
|
|
157
|
+
const k = wakeKey(team, member);
|
|
158
|
+
(p.wakeTeamFlag ??= {})[k] = Date.now();
|
|
159
|
+
savePolicy(p, home);
|
|
160
|
+
}
|
|
161
|
+
export function takeTeamFlagOnYes(p, teams, member) {
|
|
162
|
+
for (const t of teams) {
|
|
163
|
+
if (!t)
|
|
164
|
+
continue;
|
|
165
|
+
const k = wakeKey(t, member);
|
|
166
|
+
const at = p.wakeTeamFlag?.[k];
|
|
167
|
+
if (at !== undefined) {
|
|
168
|
+
delete p.wakeTeamFlag[k];
|
|
169
|
+
return Date.now() - at < 7 * 24 * 3600_000;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
120
174
|
export function isApprovedWaker(p, teams, member) {
|
|
121
175
|
return teams.some((t) => { const list = t ? p.wakers?.[t] : undefined; return Array.isArray(list) && list.includes(member); });
|
|
122
176
|
}
|
|
123
177
|
export function savePolicy(p, home) {
|
|
124
178
|
const file = policyPath(home);
|
|
125
179
|
mkdirSync(dirname(file), { recursive: true });
|
|
180
|
+
let prev = null;
|
|
181
|
+
try {
|
|
182
|
+
prev = readFileSync(file);
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
126
185
|
writeFileSync(file, JSON.stringify(p, null, 2) + "\n", { mode: 0o600 });
|
|
127
|
-
trustWrite(file);
|
|
186
|
+
trustWrite(file, prev);
|
|
128
187
|
}
|
|
129
188
|
export function useGrant(p, cap) {
|
|
130
189
|
const g = p.grants[cap];
|
|
@@ -141,6 +200,11 @@ export function grant(p, cap, mode, by) {
|
|
|
141
200
|
}
|
|
142
201
|
export function revoke(p, cap) {
|
|
143
202
|
delete p.grants[cap];
|
|
203
|
+
if (cap === "unsandboxed_runs")
|
|
204
|
+
p.denied[cap] = Date.now();
|
|
205
|
+
}
|
|
206
|
+
export function unsandboxedAllowed(p) {
|
|
207
|
+
return !!p.grants.unsandboxed_runs || p.denied.unsandboxed_runs === undefined;
|
|
144
208
|
}
|
|
145
209
|
export function mayAsk(p, cap, now = Date.now()) {
|
|
146
210
|
if (p.grants[cap])
|
|
@@ -155,22 +219,42 @@ export function recordAsk(p, cap, escalateActId) {
|
|
|
155
219
|
p.asks[cap] = { act: escalateActId, at: Date.now() };
|
|
156
220
|
}
|
|
157
221
|
export function parseGrantAnswer(body) {
|
|
158
|
-
const t = body.trim().toLowerCase();
|
|
159
|
-
if (/^(
|
|
222
|
+
const t = body.trim().toLowerCase().replace(/[.!]+$/, "").replace(/\s+/g, " ").replace(/’/g, "'");
|
|
223
|
+
if (/^(always|yes always|yes, always|always allow)$/.test(t) || /^(allow[\s-]always)\b/.test(t))
|
|
160
224
|
return "allow-always";
|
|
161
|
-
if (/^(allow([\s-]
|
|
225
|
+
if (/^(once|just this once|this once|yes once|yes, once|yes|allow|approve|approved)$/.test(t) || /^(allow[\s-](it[\s-])?once)\b/.test(t))
|
|
162
226
|
return "allow-once";
|
|
163
|
-
if (/^(deny|no|reject
|
|
227
|
+
if (/^(deny|denied|no|reject|rejected|don't allow|do not allow|don't|do not|never)\b/.test(t))
|
|
164
228
|
return "deny";
|
|
165
229
|
return null;
|
|
166
230
|
}
|
|
167
231
|
export const WOKEN_RUN_ENV = "PANTHEON_WOKEN_RUN";
|
|
168
232
|
export const inWokenRun = (env = process.env) => !!env[WOKEN_RUN_ENV];
|
|
169
|
-
export const CAPABILITY_ARGS = { self_upgrade: "self-upgrade", run_commands: "run-commands" };
|
|
233
|
+
export const CAPABILITY_ARGS = { self_upgrade: "self-upgrade", run_commands: "run-commands", unsandboxed_runs: "unsandboxed-runs" };
|
|
170
234
|
export function applyLocalAnswer(refActId, body, home, env = process.env) {
|
|
171
235
|
if (!refActId)
|
|
172
236
|
return null;
|
|
173
237
|
const p = loadPolicy(home);
|
|
238
|
+
const wa = p.wakeAsks?.[refActId];
|
|
239
|
+
if (wa) {
|
|
240
|
+
if (inWokenRun(env))
|
|
241
|
+
return { applied: false, why: "a woken run cannot let anyone wake this machine — a person has to answer it here" };
|
|
242
|
+
const verdict = parseGrantAnswer(body);
|
|
243
|
+
if (!verdict)
|
|
244
|
+
return null;
|
|
245
|
+
const k = wakeKey(wa.team, wa.member);
|
|
246
|
+
if (verdict === "allow-always")
|
|
247
|
+
setWaker(p, wa.team, wa.member, true);
|
|
248
|
+
else if (verdict === "allow-once")
|
|
249
|
+
(p.wakeOnce ??= {})[k] = Math.min(99, (p.wakeOnce[k] ?? 0) + 1);
|
|
250
|
+
else
|
|
251
|
+
(p.wakeNo ??= {})[k] = Date.now();
|
|
252
|
+
if (verdict !== "deny" && wa.teamBlocked)
|
|
253
|
+
(p.wakeTeamFlag ??= {})[k] = Date.now();
|
|
254
|
+
delete p.wakeAsks[refActId];
|
|
255
|
+
savePolicy(p, home);
|
|
256
|
+
return { applied: true, wake: { team: wa.team, member: wa.member }, verdict };
|
|
257
|
+
}
|
|
174
258
|
if (!Object.values(p.asks).some((a) => a?.act === refActId))
|
|
175
259
|
return null;
|
|
176
260
|
if (inWokenRun(env))
|
|
@@ -184,6 +268,12 @@ export function applyLocalAnswer(refActId, body, home, env = process.env) {
|
|
|
184
268
|
export function describeLocalAnswer(g) {
|
|
185
269
|
if (!g.applied)
|
|
186
270
|
return `not granted: ${g.why}`;
|
|
271
|
+
if ("wake" in g) {
|
|
272
|
+
const who = g.wake.member;
|
|
273
|
+
return g.verdict === "allow-always" ? `${who} may start work on this machine from now on — undo: pantheon wake deny ${who}`
|
|
274
|
+
: g.verdict === "allow-once" ? `${who} may start work on this machine this once — their request runs now`
|
|
275
|
+
: `${who} will not start work on this machine; their requests wait for you (not asked again for a month; pantheon wake allow ${who} to change it)`;
|
|
276
|
+
}
|
|
187
277
|
const name = CAPABILITY_ARGS[g.cap];
|
|
188
278
|
if (g.verdict === "deny")
|
|
189
279
|
return `denied ${name} on this machine — the worker will not ask again for a day`;
|