@join-pantheon/cli 0.33.4 → 0.33.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accounts.js +3 -1
- package/dist/agents.js +23 -0
- package/dist/claude-config.js +2 -1
- package/dist/cli.js +72 -13
- package/dist/client.js +84 -2
- package/dist/decision-ui.js +10 -0
- package/dist/doctor.js +12 -2
- package/dist/email-templates.js +219 -0
- package/dist/email.js +28 -65
- package/dist/history-view.js +5 -2
- package/dist/install.js +6 -4
- package/dist/mcp-instructions.js +3 -1
- package/dist/mcp.js +49 -14
- package/dist/notice.js +25 -2
- package/dist/notifier.js +31 -3
- package/dist/notify.js +2 -0
- package/dist/profile.js +71 -5
- package/dist/protocol.js +7 -2
- package/dist/quickstart.js +6 -7
- package/dist/relay.js +105 -17
- package/dist/secret-guard.js +4 -0
- package/dist/setup-guide.js +25 -5
- package/helper/macos/prebuilt/manifest.json +1 -1
- package/package.json +2 -2
package/dist/notifier.js
CHANGED
|
@@ -9,12 +9,13 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { PantheonClient } from "./client.js";
|
|
10
10
|
import { CONFIG_DIR } from "./config.js";
|
|
11
11
|
import { loadProfile } from "./profile.js";
|
|
12
|
-
import { APP_TOOLS, composeDecision, composeDelivery, composeWakeAsk, TOOL_LABELS } from "./notice.js";
|
|
12
|
+
import { APP_TOOLS, composeDecision, composeDelivery, composePassedOver, composeWakeAsk, neverAlerts, passedOverNotice, TOOL_LABELS } from "./notice.js";
|
|
13
13
|
import { loadPolicy } from "./worker-policy.js";
|
|
14
14
|
import { wakeAlerted } from "./continued.js";
|
|
15
15
|
import { cliOnPath, detectAvailable } from "./agents.js";
|
|
16
16
|
import { whichPantheon } from "./service.js";
|
|
17
17
|
export const DEFAULT_REMINDER_MS = 4 * 60 * 60 * 1_000;
|
|
18
|
+
export const PASSED_OVER_EVERY_MS = 3_600_000;
|
|
18
19
|
export const MAX_REMINDERS = 4;
|
|
19
20
|
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
20
21
|
export function wakeAskWhat(body) {
|
|
@@ -179,6 +180,11 @@ export class HumanNotifier {
|
|
|
179
180
|
for (const obligation of due) {
|
|
180
181
|
const isReminder = state.notified_at[obligation.id] !== undefined;
|
|
181
182
|
const act = client.seenAct(obligation.ref);
|
|
183
|
+
if (neverAlerts(act)) {
|
|
184
|
+
state.notified_at[obligation.id] = now();
|
|
185
|
+
dirty = true;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
182
188
|
const wake = asks[obligation.ref];
|
|
183
189
|
const workerSaid = (w) => { try {
|
|
184
190
|
return this.opts.wakeAlerted ? this.opts.wakeAlerted(w, obligation.ref) : wakeAlerted(w.team, w.member, obligation.ref);
|
|
@@ -210,12 +216,34 @@ export class HumanNotifier {
|
|
|
210
216
|
if (dirty || !existsSync(stateFile))
|
|
211
217
|
writeState(stateFile, state);
|
|
212
218
|
const arrived = await client.next(pollMs);
|
|
219
|
+
if (prefs.decisions) {
|
|
220
|
+
for (const r of arrived) {
|
|
221
|
+
if (!passedOverNotice(r, this.cfg.member) || state.notified_at[`act:${r.id}`])
|
|
222
|
+
continue;
|
|
223
|
+
state.notified_at[`act:${r.id}`] = now();
|
|
224
|
+
const g = r.meta?.goal;
|
|
225
|
+
const key = `act:passed:${typeof g === "string" ? g : r.ref ?? r.id}`;
|
|
226
|
+
const last = state.notified_at[key];
|
|
227
|
+
if (last !== undefined && now() - last < PASSED_OVER_EVERY_MS) {
|
|
228
|
+
this.opts.log(`> passed over again on ${key.slice("act:passed:".length)} — said within the hour, not alerted`);
|
|
229
|
+
writeState(stateFile, state);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const focus = client.obligations.find((o) => o.ref === r.ref)?.id;
|
|
233
|
+
const ok = await send(withLabel(composePassedOver(r, focus, now())));
|
|
234
|
+
if (ok) {
|
|
235
|
+
state.notified_at[key] = now();
|
|
236
|
+
this.opts.log(`> passed-over alert for ${r.ref ?? r.id}`);
|
|
237
|
+
}
|
|
238
|
+
writeState(stateFile, state);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
213
241
|
if (prefs.deliveries) {
|
|
214
242
|
for (const r of arrived) {
|
|
215
|
-
if (r.act !== "DELIVERED" || r.from === this.cfg.member || state.notified_at[`act:${r.id}`])
|
|
243
|
+
if (r.act !== "DELIVERED" || neverAlerts(r) || r.from === this.cfg.member || state.notified_at[`act:${r.id}`])
|
|
216
244
|
continue;
|
|
217
245
|
const goal = r.goal ? client.goals.get(r.goal) : undefined;
|
|
218
|
-
const forMe =
|
|
246
|
+
const forMe = r.to?.length ? r.to.includes(this.cfg.member) : (goal?.members.includes(this.cfg.member) ?? false);
|
|
219
247
|
if (!forMe)
|
|
220
248
|
continue;
|
|
221
249
|
const ok = await send(withLabel(composeDelivery(r, goal, peerOf(r.from), { tools, preferred: preferredTool(profile, tools), now: now() })));
|
package/dist/notify.js
CHANGED
|
@@ -391,6 +391,8 @@ export async function decideStop(raw, deps) {
|
|
|
391
391
|
const work = acts.filter((a) => {
|
|
392
392
|
if (a.goal && closed.has(a.goal))
|
|
393
393
|
return false;
|
|
394
|
+
if (a.act === "NOTE" && a.meta && typeof a.meta === "object" && "notice" in a.meta)
|
|
395
|
+
return false;
|
|
394
396
|
const to = Array.isArray(a.to) ? a.to : [];
|
|
395
397
|
if (to.length && deps.me && !to.includes(deps.me))
|
|
396
398
|
return false;
|
package/dist/profile.js
CHANGED
|
@@ -192,6 +192,34 @@ export function organizationProfile(profile, ...forSpace) {
|
|
|
192
192
|
updated_at: profile.updated_at,
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
+
export function honourClearedAuthority(cfg, relayCopy, opts = {}) {
|
|
196
|
+
const marker = relayCopy?.authority_cleared;
|
|
197
|
+
if (!marker || !relayCopy)
|
|
198
|
+
return [];
|
|
199
|
+
if (!opts.founder || marker.by !== opts.founder)
|
|
200
|
+
return [];
|
|
201
|
+
const file = opts.file ?? profilePath();
|
|
202
|
+
if (!existsSync(file))
|
|
203
|
+
return [];
|
|
204
|
+
const local = loadProfile(cfg, file);
|
|
205
|
+
const relayAt = Math.min(relayCopy.updated_at, Date.now() + 86_400_000);
|
|
206
|
+
if (!opts.force && local.updated_at > relayAt)
|
|
207
|
+
return [];
|
|
208
|
+
const keep = new Set((opts.keep ?? []).map(normalize));
|
|
209
|
+
const cleared = new Set(marker.domains.map(normalize));
|
|
210
|
+
const team = teamProfile(local, cfg.spaceId, cfg.space).authority;
|
|
211
|
+
const dropped = Object.keys(team).filter((d) => cleared.has(normalize(d)) && !keep.has(normalize(d)));
|
|
212
|
+
if (!dropped.length)
|
|
213
|
+
return [];
|
|
214
|
+
const left = Object.fromEntries(Object.entries(team).filter(([d]) => !dropped.includes(d)));
|
|
215
|
+
const key = [cfg.spaceId, cfg.space].find((k) => k && local.spaces?.[k]) ?? (cfg.spaceId || cfg.space);
|
|
216
|
+
const own = local.spaces?.[key];
|
|
217
|
+
const next = own?.authority || (opts.teams ?? 1) > 1
|
|
218
|
+
? { ...local, spaces: { ...(local.spaces ?? {}), [key]: { ...(own ?? {}), authority: left } } }
|
|
219
|
+
: { ...local, authority: left };
|
|
220
|
+
saveProfile({ ...next, updated_at: Math.max(local.updated_at + 1, Date.now()) }, file);
|
|
221
|
+
return dropped;
|
|
222
|
+
}
|
|
195
223
|
function domainMatches(rule, domain) {
|
|
196
224
|
if (rule === "*")
|
|
197
225
|
return true;
|
|
@@ -199,6 +227,15 @@ function domainMatches(rule, domain) {
|
|
|
199
227
|
return domain === rule.slice(0, -2) || domain.startsWith(rule.slice(0, -1));
|
|
200
228
|
return rule === domain;
|
|
201
229
|
}
|
|
230
|
+
export function reservedBy(profiles, domains) {
|
|
231
|
+
const out = [];
|
|
232
|
+
for (const p of profiles) {
|
|
233
|
+
const ds = domains.filter((d) => Object.entries(p.authority).some(([rule, mode]) => mode === "human_required" && domainMatches(normalize(rule), normalize(d))));
|
|
234
|
+
if (ds.length)
|
|
235
|
+
out.push({ member: p.member, domains: ds });
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
202
239
|
export function routeDecision(profiles, input) {
|
|
203
240
|
const all = [...profiles];
|
|
204
241
|
const byMember = new Map(all.map((p) => [p.member, p]));
|
|
@@ -227,7 +264,24 @@ export function routeDecision(profiles, input) {
|
|
|
227
264
|
current.domains.push(domain);
|
|
228
265
|
bucket.set(p.member, current);
|
|
229
266
|
};
|
|
230
|
-
|
|
267
|
+
const named = [...new Set((input.named ?? []).map((m) => m.trim()).filter(Boolean))];
|
|
268
|
+
const passed = new Map();
|
|
269
|
+
const reservedByOthers = (domain) => all.filter((p) => !named.includes(p.member)
|
|
270
|
+
&& Object.entries(p.authority).some(([rule, mode]) => mode === "human_required" && domainMatches(normalize(rule), domain)));
|
|
271
|
+
const toNamed = (domain) => {
|
|
272
|
+
for (const member of named) {
|
|
273
|
+
const p = byMember.get(member);
|
|
274
|
+
const own = p ? Object.entries(p.authority).filter(([rule]) => domainMatches(normalize(rule), domain)).map(([, mode]) => mode) : [];
|
|
275
|
+
add(own.includes("human_required") ? "human_required" : "agent_can_decide", p ?? { member, display_name: member, title: "" }, domain);
|
|
276
|
+
}
|
|
277
|
+
for (const p of all) {
|
|
278
|
+
if (named.includes(p.member))
|
|
279
|
+
continue;
|
|
280
|
+
if (Object.keys(p.authority).some((rule) => domainMatches(normalize(rule), domain)))
|
|
281
|
+
passed.set(p.member, [...(passed.get(p.member) ?? []), domain]);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const byDirectory = (domain) => {
|
|
231
285
|
const claims = all.flatMap((p) => {
|
|
232
286
|
const modes = [...new Set(Object.entries(p.authority)
|
|
233
287
|
.filter(([rule]) => domainMatches(normalize(rule), domain)).map(([, mode]) => mode))];
|
|
@@ -235,7 +289,7 @@ export function routeDecision(profiles, input) {
|
|
|
235
289
|
});
|
|
236
290
|
if (!claims.length) {
|
|
237
291
|
unmatched.push(domain);
|
|
238
|
-
|
|
292
|
+
return;
|
|
239
293
|
}
|
|
240
294
|
if (claims.length > 1) {
|
|
241
295
|
conflicts.push({
|
|
@@ -243,25 +297,36 @@ export function routeDecision(profiles, input) {
|
|
|
243
297
|
reason: new Set(claims.map((x) => x.mode)).size > 1 ? "mixed_authority" : "multiple_owners",
|
|
244
298
|
members: claims.map((x) => x.profile.member),
|
|
245
299
|
});
|
|
246
|
-
|
|
300
|
+
return;
|
|
247
301
|
}
|
|
248
302
|
const claim = claims[0];
|
|
249
303
|
const active = (claim.profile.delegations ?? []).filter((d) => d.starts_at <= now && now < d.expires_at && d.domains.some((rule) => domainMatches(normalize(rule), domain)));
|
|
250
304
|
if (active.length > 1) {
|
|
251
305
|
conflicts.push({ domain, reason: "multiple_delegates", members: active.map((d) => d.to) });
|
|
252
|
-
|
|
306
|
+
return;
|
|
253
307
|
}
|
|
254
308
|
if (active.length === 1) {
|
|
255
309
|
const delegated = byMember.get(active[0].to);
|
|
256
310
|
if (!delegated) {
|
|
257
311
|
conflicts.push({ domain, reason: "missing_delegate", members: [active[0].to] });
|
|
258
|
-
|
|
312
|
+
return;
|
|
259
313
|
}
|
|
260
314
|
add(claim.mode, delegated, domain);
|
|
261
315
|
}
|
|
262
316
|
else
|
|
263
317
|
add(claim.mode, claim.profile, domain);
|
|
318
|
+
};
|
|
319
|
+
for (const domain of domains) {
|
|
320
|
+
if (named.length && (input.explicit || !reservedByOthers(domain).length))
|
|
321
|
+
toNamed(domain);
|
|
322
|
+
else
|
|
323
|
+
byDirectory(domain);
|
|
264
324
|
}
|
|
325
|
+
if (named.length && !domains.length)
|
|
326
|
+
for (const member of named) {
|
|
327
|
+
const p = byMember.get(member);
|
|
328
|
+
agent.set(member, { member, display_name: p?.display_name ?? member, title: p?.title ?? "", domains: [] });
|
|
329
|
+
}
|
|
265
330
|
return {
|
|
266
331
|
domains,
|
|
267
332
|
inferred,
|
|
@@ -269,6 +334,7 @@ export function routeDecision(profiles, input) {
|
|
|
269
334
|
delegated_agents: [...agent.values()],
|
|
270
335
|
unmatched_domains: unmatched,
|
|
271
336
|
conflicts,
|
|
337
|
+
...(named.length ? { named, passed_over: [...passed].map(([member, ds]) => ({ member, domains: ds })) } : {}),
|
|
272
338
|
};
|
|
273
339
|
}
|
|
274
340
|
export function profileInitialized(profile) {
|
package/dist/protocol.js
CHANGED
|
@@ -116,7 +116,7 @@ export const GoalSigSchema = z.object({
|
|
|
116
116
|
kind: z.string().max(32).nullable().optional(),
|
|
117
117
|
cert: DelegateCertSchema.optional(),
|
|
118
118
|
});
|
|
119
|
-
export const RELAY_FEATURES = ["act-sig", "delegate"];
|
|
119
|
+
export const RELAY_FEATURES = ["act-sig", "delegate", "named-decider"];
|
|
120
120
|
export const Hello = z.object({
|
|
121
121
|
t: z.literal("hello"),
|
|
122
122
|
v: z.number().int().optional(),
|
|
@@ -160,6 +160,11 @@ export const PublishProfile = z.object({
|
|
|
160
160
|
cid: z.string().max(64).optional(),
|
|
161
161
|
profile: OrganizationProfileSchema,
|
|
162
162
|
});
|
|
163
|
+
export const ClearAuthority = z.object({
|
|
164
|
+
t: z.literal("clear_authority"),
|
|
165
|
+
cid: z.string().max(64).optional(),
|
|
166
|
+
member: z.string().min(1).max(48),
|
|
167
|
+
});
|
|
163
168
|
export const Claim = z.object({
|
|
164
169
|
t: z.literal("claim"),
|
|
165
170
|
cid: z.string().max(64).optional(),
|
|
@@ -237,7 +242,7 @@ export const WorkerState = z.object({
|
|
|
237
242
|
detail: z.string().max(200).optional(),
|
|
238
243
|
until: z.number().int().positive().optional(),
|
|
239
244
|
});
|
|
240
|
-
export const Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, ListInvites, RevokeInvite, PublishProfile, BindRepo, DeleteSpace, WorkerState, Ping]);
|
|
245
|
+
export const Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, ListInvites, RevokeInvite, PublishProfile, ClearAuthority, BindRepo, DeleteSpace, WorkerState, Ping]);
|
|
241
246
|
export function shouldDeferToHuman(sessions, work, opts = {}) {
|
|
242
247
|
const now = opts.now ?? Date.now();
|
|
243
248
|
const idleMs = opts.idleMs ?? LIMITS.workerIdleMs;
|
package/dist/quickstart.js
CHANGED
|
@@ -14,7 +14,7 @@ import { commandsChosen, loadProfile, wakeChosen, wakeDefault } from "./profile.
|
|
|
14
14
|
import { newKeypair } from "./identity.js";
|
|
15
15
|
import { installForUser } from "./install.js";
|
|
16
16
|
import { writeIntent, installHooks } from "./notify.js";
|
|
17
|
-
import { detectAvailable, agentById, agentsNeedingUnsandboxed, folderReaderLine } from "./agents.js";
|
|
17
|
+
import { detectAvailable, agentById, agentsNeedingUnsandboxed, folderReaderLine, recordedAgent } from "./agents.js";
|
|
18
18
|
import { installService, servicesUnavailable } from "./service.js";
|
|
19
19
|
import { clearTrayQuit } from "./win-host.js";
|
|
20
20
|
import { ensureNativeNotifier } from "./notifier.js";
|
|
@@ -466,13 +466,13 @@ export async function start(opts) {
|
|
|
466
466
|
const existing = priorCfg;
|
|
467
467
|
const keepIdentity = !!existing && !opts.reset && !!existing.publicKey && !!existing.privateKey;
|
|
468
468
|
let cfg = reuse
|
|
469
|
-
? { ...existing, relay: reach.url, agent: detectAgent() }
|
|
469
|
+
? { ...existing, relay: reach.url, agent: detectAgent(existing.agent) }
|
|
470
470
|
: {
|
|
471
471
|
relay: reach.url,
|
|
472
472
|
space: opts.space ?? teamNameFor(process.cwd(), repo),
|
|
473
473
|
token: randomBytes(16).toString("hex"),
|
|
474
474
|
member: opts.me ?? (keepIdentity ? existing.member : defaultMemberName()),
|
|
475
|
-
agent: detectAgent(),
|
|
475
|
+
agent: detectAgent(existing?.agent),
|
|
476
476
|
...(keepIdentity ? { publicKey: existing.publicKey, privateKey: existing.privateKey } : newKeypair()),
|
|
477
477
|
...(keepIdentity && existing.account ? { account: existing.account } : {}),
|
|
478
478
|
...(repo ? { repo: repo.fingerprint, ...(repo.origin ? { repoOrigin: repo.origin } : {}) } : {}),
|
|
@@ -590,7 +590,7 @@ export async function join(opts) {
|
|
|
590
590
|
space: opts.invite.space,
|
|
591
591
|
token: "",
|
|
592
592
|
member: opts.me ?? existing?.member ?? defaultMemberName(),
|
|
593
|
-
agent: detectAgent(),
|
|
593
|
+
agent: detectAgent(existing?.agent),
|
|
594
594
|
...keys,
|
|
595
595
|
...(existing?.account ? { account: existing.account } : {}),
|
|
596
596
|
...(repo ? { repo: repo.fingerprint, ...(repo.origin ? { repoOrigin: repo.origin } : {}) } : {}),
|
|
@@ -660,8 +660,7 @@ function temporaryFolderNote(dir, log, team) {
|
|
|
660
660
|
if (isTemporaryRefusal(r))
|
|
661
661
|
log(` folder not bound — ${r.why.replace("<team>", team)}`);
|
|
662
662
|
}
|
|
663
|
-
function detectAgent() {
|
|
664
|
-
|
|
665
|
-
return found[0]?.backend.id ?? "unknown";
|
|
663
|
+
function detectAgent(prior) {
|
|
664
|
+
return recordedAgent(prior, { fresh: true });
|
|
666
665
|
}
|
|
667
666
|
//# sourceMappingURL=quickstart.js.map
|
package/dist/relay.js
CHANGED
|
@@ -12,7 +12,7 @@ import { installerLine } from "./managed.js";
|
|
|
12
12
|
import { Ledger, SpaceFull } from "./ledger.js";
|
|
13
13
|
import { fingerprint, newNonce, verifyChallenge, verifyActivity, verifyAccount, verifyRead, actPayload, goalPayload, verifyBytes, verifyDelegate, canonicalJson } from "./identity.js";
|
|
14
14
|
import { Accounts, normalizeUsername, validEmail, validUsername } from "./accounts.js";
|
|
15
|
-
import { emailSenderFromEnv } from "./email.js";
|
|
15
|
+
import { emailSenderFromEnv, maskEmail } from "./email.js";
|
|
16
16
|
import { Allowlist, RosterUnreadable } from "./roster-file.js";
|
|
17
17
|
import { StateStore, ensurePrivateDir, setWaitForDisk, takeSlowestDiskWait, waitsForDisk } from "./store.js";
|
|
18
18
|
import { busyWriter, DiskBusy, flushWrites, onWriteError, pendingView, readLatest, recordWrites, whenLanded, writerFor } from "./disk-writer.js";
|
|
@@ -20,7 +20,8 @@ import { activity, mergeActivity, feed, noteRefusal, refusalsSince, countActs, s
|
|
|
20
20
|
import { fmtLimit, inviteLimit, parseLimits, planLimits, PLAN_NAME } from "./plans.js";
|
|
21
21
|
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
22
22
|
import { relayShellArg, teamNameProblem } from "./team-name.js";
|
|
23
|
-
import { routeDecision } from "./profile.js";
|
|
23
|
+
import { reservedBy, routeDecision } from "./profile.js";
|
|
24
|
+
import { namedMarker } from "./decision-ui.js";
|
|
24
25
|
const presentSessions = (m) => [...m.sessions.values()].filter((s) => !s.quiet);
|
|
25
26
|
function workState(m, now = Date.now(), busyTtlMs = LIMITS.activityBusyTtlMs, pruneMs = LIMITS.activityPruneMs) {
|
|
26
27
|
let known = false;
|
|
@@ -1031,7 +1032,10 @@ export class Relay {
|
|
|
1031
1032
|
: `that code is not right — ${r.tries_left ?? "a few"} ${r.tries_left === 1 ? "try" : "tries"} left on it`;
|
|
1032
1033
|
return this.json(res, 400, { error: msg, reason: r.reason, ...(r.tries_left !== undefined ? { tries_left: r.tries_left } : {}) });
|
|
1033
1034
|
}
|
|
1034
|
-
|
|
1035
|
+
this.json(res, 200, { ok: true, account: { id: r.account.id, username: r.account.username, email: r.account.email }, session: r.session.id, device: r.device.id });
|
|
1036
|
+
if (r.welcome)
|
|
1037
|
+
this.sendWelcome(r.account.email, ip);
|
|
1038
|
+
return;
|
|
1035
1039
|
});
|
|
1036
1040
|
}
|
|
1037
1041
|
if (url.pathname === "/account/sessions" && req.method === "GET") {
|
|
@@ -1180,11 +1184,11 @@ export class Relay {
|
|
|
1180
1184
|
return own ? "this device has asked for too many codes in the last hour — use the latest one, or try again in an hour"
|
|
1181
1185
|
: "too many codes were sent to this address in the last hour — use the latest one, or try again in an hour";
|
|
1182
1186
|
}
|
|
1183
|
-
if (!own && !this.localTrusted(ip) && live(this.emailSendsFrom.get(netOf(ip)) ?? []).length >= perNet)
|
|
1187
|
+
if (kind !== "welcome" && !own && !this.localTrusted(ip) && live(this.emailSendsFrom.get(netOf(ip)) ?? []).length >= perNet)
|
|
1184
1188
|
return "too many codes were requested from your network in the last hour — try again later";
|
|
1185
1189
|
if (this.emailSends.length >= cap)
|
|
1186
1190
|
return "the relay is sending too many codes right now — try again shortly";
|
|
1187
|
-
if (kind
|
|
1191
|
+
if (kind !== "login" && this.emailSends.filter((s) => s.signup).length >= cap - reserved)
|
|
1188
1192
|
return "the relay is taking too many new sign-ups right now — try again shortly";
|
|
1189
1193
|
return null;
|
|
1190
1194
|
}
|
|
@@ -1194,7 +1198,7 @@ export class Relay {
|
|
|
1194
1198
|
}
|
|
1195
1199
|
noteEmailSent(kind, email, ip, pubkey = "", own = false) {
|
|
1196
1200
|
const now = Date.now(), hour = 3_600_000;
|
|
1197
|
-
this.emailSends.push({ at: now, signup: kind
|
|
1201
|
+
this.emailSends.push({ at: now, signup: kind !== "login" });
|
|
1198
1202
|
const bump = (m, k) => {
|
|
1199
1203
|
m.set(k, [...(m.get(k) ?? []).filter((t) => now - t < hour), now]);
|
|
1200
1204
|
if (m.size > 10_000)
|
|
@@ -1207,9 +1211,26 @@ export class Relay {
|
|
|
1207
1211
|
}
|
|
1208
1212
|
};
|
|
1209
1213
|
bump(this.emailSendsTo, this.emailBucket(email, pubkey, own));
|
|
1210
|
-
if (!own && !this.localTrusted(ip))
|
|
1214
|
+
if (kind !== "welcome" && !own && !this.localTrusted(ip))
|
|
1211
1215
|
bump(this.emailSendsFrom, netOf(ip));
|
|
1212
1216
|
}
|
|
1217
|
+
sendWelcome(to, ip) {
|
|
1218
|
+
if (!this.email.sendWelcome)
|
|
1219
|
+
return;
|
|
1220
|
+
if (this.emailRefusal("welcome", to, ip)) {
|
|
1221
|
+
this.log(`welcome email to ${maskEmail(to)} skipped: the email budget is spent`);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
let sending;
|
|
1225
|
+
try {
|
|
1226
|
+
sending = Promise.resolve(this.email.sendWelcome(to));
|
|
1227
|
+
}
|
|
1228
|
+
catch {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
sending.then((sent) => { if (sent)
|
|
1232
|
+
this.noteEmailSent("welcome", to, ip); }, () => { });
|
|
1233
|
+
}
|
|
1213
1234
|
authRateOk(ip) {
|
|
1214
1235
|
if (this.localTrusted(ip))
|
|
1215
1236
|
return true;
|
|
@@ -3223,6 +3244,29 @@ pantheon start</code></pre>
|
|
|
3223
3244
|
this.send(ws, { t: "ack", ...(m.cid ? { cid: m.cid } : {}), id: me.id, seq: space.ledger.head });
|
|
3224
3245
|
return;
|
|
3225
3246
|
}
|
|
3247
|
+
if (m.t === "clear_authority") {
|
|
3248
|
+
const founder = space.allow.founder();
|
|
3249
|
+
if (!founder || me.id !== founder)
|
|
3250
|
+
return fail("FOUNDER_ONLY", `only ${founder ?? "the team's founder"}, who founded "${space.name}", can clear someone's decision claims`, m.cid);
|
|
3251
|
+
if (!this.writeOk(me))
|
|
3252
|
+
return fail("RATE_LIMIT", "too many changes too fast — wait a few seconds", m.cid);
|
|
3253
|
+
const current = space.profiles.get(m.member);
|
|
3254
|
+
const domains = Object.keys(current?.authority ?? {});
|
|
3255
|
+
if (!current || !domains.length)
|
|
3256
|
+
return fail("NO_CLAIMS", `${m.member} claims no decisions in "${space.name}" — nothing to clear`, m.cid);
|
|
3257
|
+
const cleared = { ...current, authority: {}, updated_at: current.updated_at + 1, authority_cleared: { by: me.id, at: Date.now(), domains } };
|
|
3258
|
+
space.profiles.set(m.member, cleared);
|
|
3259
|
+
space.store.flush(space.goals, space.obligations, space.profiles);
|
|
3260
|
+
this.broadcast(space, { t: "profiles", profiles: [...space.profiles.values()] });
|
|
3261
|
+
const listed = domains.length > 4 ? `${domains.slice(0, 4).join(", ")} …` : domains.join(", ");
|
|
3262
|
+
const note = this.commit(space, {
|
|
3263
|
+
from: me.id, session: sessionId, role, act: "NOTE", to: null, goal: null, ref: null,
|
|
3264
|
+
body: `${me.id} cleared ${m.member}'s decision claims (${listed}); ${m.member} can set them again with pantheon setup --decides. An older Pantheon on ${m.member}'s machine may restore them on its next profile change`,
|
|
3265
|
+
meta: { organization_profile: cleared },
|
|
3266
|
+
});
|
|
3267
|
+
this.send(ws, { t: "ack", ...(m.cid ? { cid: m.cid } : {}), id: note.id, seq: space.ledger.head });
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3226
3270
|
if (m.t === "admit") {
|
|
3227
3271
|
const self = space.allow.get(me.id);
|
|
3228
3272
|
if (!self?.admitted)
|
|
@@ -3621,6 +3665,17 @@ pantheon start</code></pre>
|
|
|
3621
3665
|
space.recent.delete(k);
|
|
3622
3666
|
}
|
|
3623
3667
|
this.send(ws, { t: "ack", ...(m.cid ? { cid: m.cid } : {}), id: rec.id, seq: rec.seq });
|
|
3668
|
+
const passedOver = rec.act === "HUMAN_REQUIRED" && rec.meta?.["authority"] === "named" ? passedOverBy(space, rec.to[0], decisionDomains(rec)) : [];
|
|
3669
|
+
const goalTitle = rec.goal ? space.goals.get(rec.goal)?.title : undefined;
|
|
3670
|
+
for (const p of passedOver) {
|
|
3671
|
+
if (!space.members.has(p.member))
|
|
3672
|
+
continue;
|
|
3673
|
+
this.commit(space, {
|
|
3674
|
+
from: me.id, session: sessionId, role, act: "NOTE", to: [p.member], goal: null, ref: rec.id,
|
|
3675
|
+
body: `${me.id} named ${rec.to[0]} to decide ${p.domains.join(", ")} on '${goalTitle ?? first(rec.body, 80)}' — you were passed over`,
|
|
3676
|
+
meta: { notice: "passed_over", decider: rec.to[0], domains: p.domains, ...(rec.goal ? { goal: rec.goal } : {}) },
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3624
3679
|
if (recoveryStep) {
|
|
3625
3680
|
const { action, member: who, pend } = recoveryStep;
|
|
3626
3681
|
if (space.allow.recoveryOf(who)?.pubkey === pend.pubkey) {
|
|
@@ -3831,20 +3886,40 @@ pantheon start</code></pre>
|
|
|
3831
3886
|
message: "HUMAN_REQUIRED must name decision_domains so the relay can verify the human owner. Call pantheon_route_decision first." };
|
|
3832
3887
|
}
|
|
3833
3888
|
const target = sp.profiles.get(to[0]);
|
|
3834
|
-
const
|
|
3835
|
-
|
|
3889
|
+
const flagged = meta?.["named_by_requester"] === true;
|
|
3890
|
+
const passed = flagged ? passedOverBy(sp, to[0], domains) : [];
|
|
3891
|
+
if (flagged && role === "worker" && passed.length) {
|
|
3892
|
+
return { code: "NAMED_BY_A_RUN",
|
|
3893
|
+
message: "only a person, in their own session, can name who decides over the directory — a woken run routes each domain to its published owner (pantheon_route_decision)" };
|
|
3894
|
+
}
|
|
3895
|
+
const namedBy = flagged && role !== "worker";
|
|
3896
|
+
if (namedBy) {
|
|
3897
|
+
const own = reservedBy(target ? [target] : [], domains)[0]?.domains ?? [];
|
|
3898
|
+
const uncovered = domains.filter((d) => !own.includes(d));
|
|
3899
|
+
if (passed.length) {
|
|
3900
|
+
meta["authority"] = "named";
|
|
3901
|
+
meta["unverified_domains"] = uncovered;
|
|
3902
|
+
}
|
|
3903
|
+
else if (uncovered.length) {
|
|
3904
|
+
meta["authority"] = target ? "partly_unverified" : "unverified";
|
|
3905
|
+
meta["unverified_domains"] = uncovered;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
const route = namedBy ? null : routeDecision(sp.profiles.values(), { domains });
|
|
3909
|
+
if (route && route.conflicts.length) {
|
|
3836
3910
|
return { code: "AMBIGUOUS_DECISION_AUTHORITY",
|
|
3837
3911
|
message: `authority conflicts for ${route.conflicts.map((c) => `${c.domain} (${c.reason}: ${c.members.join(", ")})`).join("; ")}. Fix the profiles or ESCALATE instead of guessing.` };
|
|
3838
3912
|
}
|
|
3839
|
-
const others = route
|
|
3913
|
+
const others = (route?.required_humans ?? []).filter((h) => h.member !== to[0] && h.domains.some((d) => domains.includes(d)));
|
|
3840
3914
|
if (others.length) {
|
|
3841
3915
|
const theirs = [...new Set(others.flatMap((h) => h.domains.filter((d) => domains.includes(d))))];
|
|
3842
3916
|
return { code: "WRONG_DECISION_OWNER",
|
|
3843
|
-
message: `${to[0]} is not the human-required owner for: ${theirs.join(", ")} — ${others.map((h) => h.member).join(", ")} ${others.length === 1 ? "is" : "are"}. Call pantheon_route_decision and route each domain to its published owner.`
|
|
3917
|
+
message: `${to[0]} is not the human-required owner for: ${theirs.join(", ")} — ${others.map((h) => h.member).join(", ")} ${others.length === 1 ? "is" : "are"}. Call pantheon_route_decision and route each domain to its published owner.` +
|
|
3918
|
+
(role === "worker" ? "" : ` If the person who asked for this work named ${to[0]} to decide, send it again with named_by_requester: true — a name wins over the directory.`) };
|
|
3844
3919
|
}
|
|
3845
|
-
const owner = route
|
|
3920
|
+
const owner = route?.required_humans.find((x) => x.member === to[0]);
|
|
3846
3921
|
const uncovered = domains.filter((d) => !owner?.domains.includes(d));
|
|
3847
|
-
if (uncovered.length && meta) {
|
|
3922
|
+
if (route && uncovered.length && meta) {
|
|
3848
3923
|
meta["authority"] = target ? "partly_unverified" : "unverified";
|
|
3849
3924
|
meta["unverified_domains"] = uncovered;
|
|
3850
3925
|
}
|
|
@@ -4129,7 +4204,7 @@ pantheon start</code></pre>
|
|
|
4129
4204
|
break;
|
|
4130
4205
|
case "HUMAN_REQUIRED":
|
|
4131
4206
|
this.oblige(sp, { kind: "HUMAN", owner: `human:${rec.to[0]}`, ref: rec.id, goal: rec.goal,
|
|
4132
|
-
body:
|
|
4207
|
+
body: decisionBody(rec, sp) });
|
|
4133
4208
|
if (goal && goal.state === "OPEN") {
|
|
4134
4209
|
goal.state = "BLOCKED";
|
|
4135
4210
|
this.broadcast(sp, { t: "goal", goal });
|
|
@@ -4157,7 +4232,7 @@ pantheon start</code></pre>
|
|
|
4157
4232
|
if (asked && decider === rec.from && !stillAnswered && !alreadyLive && !retracted.has(asked.id)) {
|
|
4158
4233
|
this.oblige(sp, { kind: "HUMAN", owner: `human:${decider}`, ref: asked.id, goal: asked.goal,
|
|
4159
4234
|
...(asked.act === "ESCALATE" && asked.ref ? { about: asked.ref } : {}),
|
|
4160
|
-
body: asked.act === "ESCALATE" ? `${asked.from} escalated: ${first(asked.body)}` :
|
|
4235
|
+
body: asked.act === "ESCALATE" ? `${asked.from} escalated: ${first(asked.body)}` : decisionBody(asked, sp) });
|
|
4161
4236
|
const g = asked.goal ? sp.goals.get(asked.goal) : undefined;
|
|
4162
4237
|
if (g && g.state === "OPEN") {
|
|
4163
4238
|
g.state = "BLOCKED";
|
|
@@ -4427,7 +4502,7 @@ pantheon start</code></pre>
|
|
|
4427
4502
|
}
|
|
4428
4503
|
for (const e of allow.all()) {
|
|
4429
4504
|
if (e.admitted && !sp.members.has(e.member)) {
|
|
4430
|
-
sp.members.set(e.member, { id: e.member, display: e.member, agent: "unknown", last_seen: e.first_seen, sessions: new Map(), activity: new Map(), delivered_seq: 0, stamps: [] });
|
|
4505
|
+
sp.members.set(e.member, { id: e.member, display: e.member, agent: e.last_agent ?? "unknown", last_seen: e.first_seen, sessions: new Map(), activity: new Map(), delivered_seq: 0, stamps: [] });
|
|
4431
4506
|
}
|
|
4432
4507
|
}
|
|
4433
4508
|
this.rebuildRecent(sp);
|
|
@@ -4578,7 +4653,9 @@ pantheon start</code></pre>
|
|
|
4578
4653
|
return m;
|
|
4579
4654
|
}
|
|
4580
4655
|
m.display = display;
|
|
4581
|
-
|
|
4656
|
+
const known = agent && agent !== "unknown";
|
|
4657
|
+
if (known || !m.agent || m.agent === "unknown")
|
|
4658
|
+
m.agent = agent;
|
|
4582
4659
|
m.last_seen = Date.now();
|
|
4583
4660
|
m.sessions.set(session, { id: session, role, agent, ...(pkg ? { pkg } : {}), ws });
|
|
4584
4661
|
sp.allow.touch(id, pkg, agent);
|
|
@@ -4686,6 +4763,17 @@ pantheon start</code></pre>
|
|
|
4686
4763
|
this.hangUp(failed.ws, "STORAGE");
|
|
4687
4764
|
}
|
|
4688
4765
|
}
|
|
4766
|
+
function decisionDomains(rec) {
|
|
4767
|
+
const d = rec.meta?.["decision_domains"];
|
|
4768
|
+
return Array.isArray(d) ? [...new Set(d.filter((x) => typeof x === "string" && x.trim().length > 0))] : [];
|
|
4769
|
+
}
|
|
4770
|
+
function passedOverBy(sp, decider, domains) {
|
|
4771
|
+
return reservedBy([...sp.profiles.values()].filter((p) => p.member !== decider), domains);
|
|
4772
|
+
}
|
|
4773
|
+
function decisionBody(rec, sp) {
|
|
4774
|
+
const marker = rec.meta?.["authority"] === "named" ? namedMarker(rec.from, rec.meta, passedOverBy(sp, rec.to?.[0] ?? "", decisionDomains(rec))) : null;
|
|
4775
|
+
return `${rec.from} needs a decision: ${marker ? `[${marker}] ` : ""}${first(rec.body)}`;
|
|
4776
|
+
}
|
|
4689
4777
|
function first(s, n = 120) {
|
|
4690
4778
|
const line = s.split("\n")[0] ?? "";
|
|
4691
4779
|
return line.length > n ? line.slice(0, n - 1) + "…" : line;
|
package/dist/secret-guard.js
CHANGED
|
@@ -138,4 +138,8 @@ export function refuseSecrets(guard, fields) {
|
|
|
138
138
|
}
|
|
139
139
|
return null;
|
|
140
140
|
}
|
|
141
|
+
export const PRIVATE_ARTIFACT_NOTE = "claude.ai artifact links are private to the sender's Claude account; teammates on other accounts get 'Page not found'. Share files through the repo, Jira, Confluence or Drive instead.";
|
|
142
|
+
export function privateLinkNote(...texts) {
|
|
143
|
+
return texts.some((t) => typeof t === "string" && /claude\.ai\/(?:artifact\/|public\/artifacts)/i.test(t)) ? PRIVATE_ARTIFACT_NOTE : null;
|
|
144
|
+
}
|
|
141
145
|
//# sourceMappingURL=secret-guard.js.map
|
package/dist/setup-guide.js
CHANGED
|
@@ -3,7 +3,7 @@ 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, wakeMode } from "./profile.js";
|
|
6
|
+
import { applyProfileInit, loadProfile, profilePath, saveProfile, spaceOverride, teamProfile, 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";
|
|
@@ -47,7 +47,7 @@ export function guideDefaults(profile, cfg) {
|
|
|
47
47
|
approval: profile.agent.approval ?? "approve",
|
|
48
48
|
name: profile.human.display_name || cfg.display || gitUserName() || cfg.member,
|
|
49
49
|
title: profile.human.title || "",
|
|
50
|
-
decides: Object.entries(profile.authority).filter(([, m]) => m === "human_required").map(([d]) => d),
|
|
50
|
+
decides: Object.entries(teamProfile(profile, cfg.spaceId, cfg.space).authority).filter(([, m]) => m === "human_required").map(([d]) => d),
|
|
51
51
|
notifications: !n.decisions ? "none" : n.worker_done ? "worker" : n.deliveries ? "deliveries" : "decisions",
|
|
52
52
|
persona: false,
|
|
53
53
|
};
|
|
@@ -169,8 +169,21 @@ export function parseSetupFlags(argv) {
|
|
|
169
169
|
const base = val("base");
|
|
170
170
|
if (base !== undefined && base.trim())
|
|
171
171
|
preset.base = base.trim();
|
|
172
|
+
for (const f of PROFILE_ONLY_FLAGS) {
|
|
173
|
+
if (argv.some((x) => x === `--${f}` || x.startsWith(`--${f}=`)))
|
|
174
|
+
errors.push(`--${f} is a profile field, not a setup answer — nothing was changed. Use pantheon profile set --${f} …${f === "human-required" || f === "agent-can-decide" ? " (decision areas like engineering,product)" : ""}`);
|
|
175
|
+
}
|
|
172
176
|
return { preset, errors };
|
|
173
177
|
}
|
|
178
|
+
export const PROFILE_ONLY_FLAGS = ["responsibilities", "human-required", "agent-can-decide", "capabilities"];
|
|
179
|
+
export function decisionAreasProblem(flagName, raw) {
|
|
180
|
+
if (raw === undefined)
|
|
181
|
+
return null;
|
|
182
|
+
const items = raw.split(",").map((x) => x.trim()).filter(Boolean);
|
|
183
|
+
if (!raw.includes(";") && !items.some((x) => /\s/.test(x)))
|
|
184
|
+
return null;
|
|
185
|
+
return `--${flagName} "${raw}": these take decision areas like engineering,product; describe the role with --responsibilities`;
|
|
186
|
+
}
|
|
174
187
|
export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
175
188
|
const optOut = ctx.optOut ?? {};
|
|
176
189
|
const serves = ctx.project ? { path: ctx.project, repos: true } : ctx.serves ?? null;
|
|
@@ -267,7 +280,8 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
267
280
|
}
|
|
268
281
|
}
|
|
269
282
|
let next = a.title ? applyProfileInit(profile, { name: a.name, title: a.title }) : { ...profile, human: { ...profile.human, display_name: a.name } };
|
|
270
|
-
const
|
|
283
|
+
const ovKey = [ctx.cfg.spaceId, ctx.cfg.space].find((k) => k && spaceOverride(next, k)?.authority);
|
|
284
|
+
const authority = { ...(ovKey ? next.spaces[ovKey].authority : next.authority) };
|
|
271
285
|
const prior = guideDefaults(profile, ctx.cfg).decides;
|
|
272
286
|
const replace = !!io || preset.decides !== undefined;
|
|
273
287
|
for (const area of DECISION_AREAS) {
|
|
@@ -278,7 +292,7 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
278
292
|
}
|
|
279
293
|
next = {
|
|
280
294
|
...next,
|
|
281
|
-
authority,
|
|
295
|
+
...(ovKey ? { spaces: { ...next.spaces, [ovKey]: { ...next.spaces[ovKey], authority } } } : { authority }),
|
|
282
296
|
agent: { ...next.agent, wake_enabled: a.wake !== "no", wake_default: a.wake, allow_bash: a.commands, approval: a.approval },
|
|
283
297
|
notifications: {
|
|
284
298
|
decisions: a.notifications !== "none",
|
|
@@ -291,8 +305,14 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
291
305
|
applied.push(`profile: ${next.human.display_name}${next.human.title ? `, ${next.human.title}` : ""}${a.decides.length ? ` — you personally decide ${a.decides.join(", ")}` : " — no decisions reserved for you; your agent may decide"}`);
|
|
292
306
|
if (deps.publish) {
|
|
293
307
|
try {
|
|
294
|
-
await deps.publish(next);
|
|
308
|
+
const team = await deps.publish(next, a.decides);
|
|
295
309
|
applied.push("profile published to your team, so decisions route to you by name");
|
|
310
|
+
if (Array.isArray(team)) {
|
|
311
|
+
const held = (d) => team.some((p) => p.member !== ctx.cfg.member && Object.keys(p.authority).includes(d));
|
|
312
|
+
for (const d of a.decides.filter((x) => !prior.includes(x) && !held(x))) {
|
|
313
|
+
warnings.push(`"${d}": every ${d} decision in ${ctx.cfg.space} will wait for you — nobody else holds it. If that is more than you meant, pantheon setup --decides ${a.decides.filter((x) => x !== d).join(",") || "none"}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
296
316
|
}
|
|
297
317
|
catch (e) {
|
|
298
318
|
warnings.push(`profile saved but not published (${e.message}) — run \`pantheon profile publish\` when the relay is reachable`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.5",
|
|
4
4
|
"description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"test:installer": "node scripts/installer-tests/run.mjs"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
28
28
|
"undici": "^7.29.1",
|
|
29
29
|
"ws": "^8.18.0",
|
|
30
30
|
"zod": "^3.23.8"
|