@stage5/lumine 0.2.60 → 0.2.61
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 +7 -0
- package/lib/admin.js +133 -6
- package/lib/commands.js +6 -6
- package/lib/sponsor-duty.js +46 -0
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +35 -0
package/README.md
CHANGED
|
@@ -276,6 +276,13 @@ heartbeat, supervisor, or provider-spawning daemon is not sponsor duty. The CLI
|
|
|
276
276
|
rejects a provider name that differs from the agent session actually running
|
|
277
277
|
the command.
|
|
278
278
|
|
|
279
|
+
`duty watch` may return a `teamAccessRequest` instead of a job. Tell the
|
|
280
|
+
sponsor which requester, owner, and project issued the invitation, then return
|
|
281
|
+
to duty without waiting or accepting on the sponsor's behalf. The ordinary
|
|
282
|
+
Twinkle team invitation is already waiting for the sponsor; no Workshop work
|
|
283
|
+
starts unless they accept it there. This notice is invitation metadata only;
|
|
284
|
+
it does not expose the unapproved Workshop plan or private chat.
|
|
285
|
+
|
|
279
286
|
When a user approves a plan, `duty watch` returns the scoped assignment file and
|
|
280
287
|
contribution workspace to that same session. It does not launch a fresh Codex
|
|
281
288
|
or Claude process. The on-duty agent uses this explicit lifecycle while it
|
package/lib/admin.js
CHANGED
|
@@ -310,6 +310,9 @@ export async function adminCommand(options) {
|
|
|
310
310
|
typeof operation.body.buildReviewUnderstanding === "string",
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
+
if (operation.name === "ai-email-policy.set") {
|
|
314
|
+
assertAiEmailPolicySetResult({ operation, result });
|
|
315
|
+
}
|
|
313
316
|
if (operation.name === "daily-run.start") {
|
|
314
317
|
assertAdminTodoHandoffResult(result);
|
|
315
318
|
}
|
|
@@ -493,6 +496,55 @@ export function assertComposedCommentDraftResult({
|
|
|
493
496
|
throw error;
|
|
494
497
|
}
|
|
495
498
|
|
|
499
|
+
export function assertAiEmailPolicySetResult({ operation, result }) {
|
|
500
|
+
const requestedEmail = String(operation?.body?.email || "");
|
|
501
|
+
const requestedMode = String(operation?.body?.mode || "");
|
|
502
|
+
const policy = result?.data?.policy;
|
|
503
|
+
const projection = result?.data?.projection;
|
|
504
|
+
const accountUserIds = Array.isArray(result?.data?.accountUserIds)
|
|
505
|
+
? result.data.accountUserIds
|
|
506
|
+
: [];
|
|
507
|
+
const accountCount = Number(result?.data?.accountCount);
|
|
508
|
+
const expectedIdentityType =
|
|
509
|
+
requestedMode === "separate_accounts"
|
|
510
|
+
? "separate_verified_email"
|
|
511
|
+
: "verified_email";
|
|
512
|
+
if (
|
|
513
|
+
policy?.exists === true &&
|
|
514
|
+
policy?.normalizedEmail === requestedEmail &&
|
|
515
|
+
policy?.mode === requestedMode &&
|
|
516
|
+
Number.isSafeInteger(accountCount) &&
|
|
517
|
+
accountCount >= 0 &&
|
|
518
|
+
accountUserIds.length === accountCount &&
|
|
519
|
+
projection?.expectedIdentityType === expectedIdentityType &&
|
|
520
|
+
projection?.accountCount === accountCount &&
|
|
521
|
+
projection?.matchingAccountCount === accountCount &&
|
|
522
|
+
Array.isArray(projection?.mismatchedAccountUserIds) &&
|
|
523
|
+
projection.mismatchedAccountUserIds.length === 0 &&
|
|
524
|
+
projection?.converged === true
|
|
525
|
+
) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const error = new Error(
|
|
529
|
+
"The API did not confirm that every matching account converged to the requested AI email policy. Retry only after reviewing the canonical response and deployed API.",
|
|
530
|
+
);
|
|
531
|
+
error.code = "LUMINE_ADMIN_AI_EMAIL_POLICY_NOT_CONVERGED";
|
|
532
|
+
error.data = {
|
|
533
|
+
ok: false,
|
|
534
|
+
status: "partial_failure",
|
|
535
|
+
error: {
|
|
536
|
+
code: error.code,
|
|
537
|
+
message: error.message,
|
|
538
|
+
details: {
|
|
539
|
+
requestedEmail,
|
|
540
|
+
requestedMode,
|
|
541
|
+
canonical: result?.data || null,
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
|
|
496
548
|
const RECOMMENDATION_CONTENT_TYPES = new Map([
|
|
497
549
|
["comment", "comment"],
|
|
498
550
|
["aistory", "aiStory"],
|
|
@@ -616,6 +668,7 @@ function adminOperationRequiresRun(operation) {
|
|
|
616
668
|
"notable.add",
|
|
617
669
|
].includes(operation.name) &&
|
|
618
670
|
!operation.name.startsWith("ai-bucket.") &&
|
|
671
|
+
!operation.name.startsWith("ai-email-policy.") &&
|
|
619
672
|
!operation.name.startsWith("todo.")
|
|
620
673
|
);
|
|
621
674
|
}
|
|
@@ -774,6 +827,30 @@ export function parseAdminOperation(options) {
|
|
|
774
827
|
}
|
|
775
828
|
}
|
|
776
829
|
|
|
830
|
+
if (namespace === "ai-email-policy" || namespace === "ai-email-policies") {
|
|
831
|
+
const email = parseAiEmailPolicyEmail(options.adminEmail);
|
|
832
|
+
if (action === "get" || action === "status") {
|
|
833
|
+
return bodyReadOperation(
|
|
834
|
+
"ai-email-policy.get",
|
|
835
|
+
"POST",
|
|
836
|
+
"/cli/admin/ai-email-policies/lookup",
|
|
837
|
+
{ email },
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
if (action === "set") {
|
|
841
|
+
return writeOperation(
|
|
842
|
+
"ai-email-policy.set",
|
|
843
|
+
"PUT",
|
|
844
|
+
"/cli/admin/ai-email-policies",
|
|
845
|
+
{
|
|
846
|
+
email,
|
|
847
|
+
mode: parseAiEmailPolicyMode(options.adminMode),
|
|
848
|
+
note: parseAiEmailPolicyNote(options.note),
|
|
849
|
+
},
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
777
854
|
if (namespace === "todo" || namespace === "todos") {
|
|
778
855
|
if (!action || action === "list") {
|
|
779
856
|
return readOperation(
|
|
@@ -1985,6 +2062,10 @@ function readOperation(name, path, extra = {}) {
|
|
|
1985
2062
|
};
|
|
1986
2063
|
}
|
|
1987
2064
|
|
|
2065
|
+
function bodyReadOperation(name, method, path, body) {
|
|
2066
|
+
return { name, method, path, body, mutates: false };
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1988
2069
|
function writeOperation(name, method, path, body) {
|
|
1989
2070
|
return { name, method, path, body, mutates: true };
|
|
1990
2071
|
}
|
|
@@ -2156,6 +2237,49 @@ function parseAiBucketUserIds(value) {
|
|
|
2156
2237
|
return ids;
|
|
2157
2238
|
}
|
|
2158
2239
|
|
|
2240
|
+
function parseAiEmailPolicyEmail(value) {
|
|
2241
|
+
const email = String(value || "")
|
|
2242
|
+
.trim()
|
|
2243
|
+
.toLowerCase();
|
|
2244
|
+
const at = email.indexOf("@");
|
|
2245
|
+
if (
|
|
2246
|
+
!email ||
|
|
2247
|
+
email.length > 320 ||
|
|
2248
|
+
/\s/.test(email) ||
|
|
2249
|
+
at <= 0 ||
|
|
2250
|
+
at !== email.lastIndexOf("@") ||
|
|
2251
|
+
at === email.length - 1
|
|
2252
|
+
) {
|
|
2253
|
+
throw cliValidationError(
|
|
2254
|
+
"Pass a valid verified email with --email <address>.",
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
return email;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
function parseAiEmailPolicyMode(value) {
|
|
2261
|
+
const mode = String(value || "")
|
|
2262
|
+
.trim()
|
|
2263
|
+
.toLowerCase();
|
|
2264
|
+
if (mode !== "automatic" && mode !== "separate_accounts") {
|
|
2265
|
+
throw cliValidationError("--mode must be automatic or separate_accounts.");
|
|
2266
|
+
}
|
|
2267
|
+
return mode;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function parseAiEmailPolicyNote(value) {
|
|
2271
|
+
const note = String(value || "").trim();
|
|
2272
|
+
if (!note) {
|
|
2273
|
+
throw cliValidationError("Pass the policy reason with --note <text>.");
|
|
2274
|
+
}
|
|
2275
|
+
if (note.length > 255) {
|
|
2276
|
+
throw cliValidationError(
|
|
2277
|
+
"An AI email-policy note can be at most 255 characters.",
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
return note;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2159
2283
|
function parseRequiredInteger(
|
|
2160
2284
|
value,
|
|
2161
2285
|
label,
|
|
@@ -2421,7 +2545,9 @@ async function printSpooledAdminResult({ result, storage }) {
|
|
|
2421
2545
|
);
|
|
2422
2546
|
});
|
|
2423
2547
|
} else if (storage.collectionKey === "comments") {
|
|
2424
|
-
console.log(
|
|
2548
|
+
console.log(
|
|
2549
|
+
`${count} comment(s) for ${data.subject?.url || "this target"}:`,
|
|
2550
|
+
);
|
|
2425
2551
|
await forEachPaginatedResultItem(result, async (comment) => {
|
|
2426
2552
|
console.log(
|
|
2427
2553
|
`#${comment.id} ${comment.author?.username || "unknown"}: ${comment.content || "(empty)"}`,
|
|
@@ -2502,11 +2628,12 @@ function printAdminResult({ operation, result }) {
|
|
|
2502
2628
|
console.log(
|
|
2503
2629
|
`Sponsor-integrity case #${item.id}: ${String(item.status).toUpperCase()} · job #${item.job?.id} · ${item.sponsorUsername || `sponsor ${item.sponsorUserId}`}.`,
|
|
2504
2630
|
);
|
|
2505
|
-
const relays =
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2631
|
+
const relays =
|
|
2632
|
+
Array.isArray(item.relays) && item.relays.length > 0
|
|
2633
|
+
? item.relays
|
|
2634
|
+
: item.relay
|
|
2635
|
+
? [item.relay]
|
|
2636
|
+
: [];
|
|
2510
2637
|
console.log(
|
|
2511
2638
|
`Relays: ${relays.map((relay) => `${relay.kind || "initial_request"}: ${relay.summary || "(missing)"}`).join(" | ") || "(missing)"}`,
|
|
2512
2639
|
);
|
package/lib/commands.js
CHANGED
|
@@ -2411,12 +2411,8 @@ export function parseArgs(args) {
|
|
|
2411
2411
|
sponsorFailureReason: raw.reason ? String(raw.reason) : "",
|
|
2412
2412
|
sponsorUpdateFile: raw.file ? String(raw.file) : "",
|
|
2413
2413
|
sponsorUpdatePhase: raw.phase ? String(raw.phase) : "",
|
|
2414
|
-
sponsorResolvedModel: raw.resolvedModel
|
|
2415
|
-
|
|
2416
|
-
: "",
|
|
2417
|
-
sponsorResolvedEffort: raw.resolvedEffort
|
|
2418
|
-
? String(raw.resolvedEffort)
|
|
2419
|
-
: "",
|
|
2414
|
+
sponsorResolvedModel: raw.resolvedModel ? String(raw.resolvedModel) : "",
|
|
2415
|
+
sponsorResolvedEffort: raw.resolvedEffort ? String(raw.resolvedEffort) : "",
|
|
2420
2416
|
sponsorResolvedServiceTier: raw.resolvedServiceTier
|
|
2421
2417
|
? String(raw.resolvedServiceTier)
|
|
2422
2418
|
: "",
|
|
@@ -2442,6 +2438,8 @@ export function parseArgs(args) {
|
|
|
2442
2438
|
adminBucketId: raw.bucketId ? String(raw.bucketId) : "",
|
|
2443
2439
|
adminLabel: raw.label ? String(raw.label) : "",
|
|
2444
2440
|
adminUserIds: raw.userIds ? String(raw.userIds) : "",
|
|
2441
|
+
adminEmail: raw.email ? String(raw.email) : "",
|
|
2442
|
+
adminMode: raw.mode ? String(raw.mode) : "",
|
|
2445
2443
|
adminType: raw.type ? String(raw.type) : "",
|
|
2446
2444
|
adminKind: raw.kind ? String(raw.kind) : "",
|
|
2447
2445
|
adminContentTypes: raw.contentTypes ? String(raw.contentTypes) : "",
|
|
@@ -2807,6 +2805,8 @@ export function printHelp() {
|
|
|
2807
2805
|
lumine admin ai-bucket get --bucket-id <id> [--json]
|
|
2808
2806
|
lumine admin ai-bucket accounts add --bucket-id <id> --user-ids <id,id,...> [--note <text>] [--json]
|
|
2809
2807
|
lumine admin ai-bucket note set --bucket-id <id> --note <text> [--json]
|
|
2808
|
+
lumine admin ai-email-policy get --email <address> [--json]
|
|
2809
|
+
lumine admin ai-email-policy set --email <address> --mode <automatic|separate_accounts> --note <text> [--json]
|
|
2810
2810
|
lumine admin daily-run start [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
|
|
2811
2811
|
lumine admin daily-run status|report|complete|fail [--reason <text>] [--json]
|
|
2812
2812
|
lumine admin daily-run escalation add --target <target> --note <summary> [--severity attention|urgent] [--json]
|
package/lib/sponsor-duty.js
CHANGED
|
@@ -21,6 +21,18 @@ const DEFAULT_DUTY_WATCH_MS = 20_000;
|
|
|
21
21
|
const MAX_DUTY_WATCH_MS = 60_000;
|
|
22
22
|
const MAX_FORUM_CONTEXT_CHARS = 8_000;
|
|
23
23
|
|
|
24
|
+
function sanitizeSponsorTerminalLabel(value, fallback) {
|
|
25
|
+
const label = String(value || "")
|
|
26
|
+
.replace(
|
|
27
|
+
/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,
|
|
28
|
+
" ",
|
|
29
|
+
)
|
|
30
|
+
.replace(/\s+/g, " ")
|
|
31
|
+
.trim()
|
|
32
|
+
.slice(0, 200);
|
|
33
|
+
return label || fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export async function sponsorDutyCommand(options, args, commandServices) {
|
|
25
37
|
const action = String(args[0] || "status")
|
|
26
38
|
.trim()
|
|
@@ -350,6 +362,40 @@ async function watchDuty(options, commandServices) {
|
|
|
350
362
|
);
|
|
351
363
|
continue;
|
|
352
364
|
}
|
|
365
|
+
if (claim.teamAccessRequest) {
|
|
366
|
+
const request = claim.teamAccessRequest;
|
|
367
|
+
const requester = request.requesterUsername
|
|
368
|
+
? `@${sanitizeSponsorTerminalLabel(
|
|
369
|
+
request.requesterUsername,
|
|
370
|
+
`user-${request.requesterUserId}`,
|
|
371
|
+
)}`
|
|
372
|
+
: `user #${request.requesterUserId}`;
|
|
373
|
+
const owner = request.ownerUsername
|
|
374
|
+
? `@${sanitizeSponsorTerminalLabel(
|
|
375
|
+
request.ownerUsername,
|
|
376
|
+
`user-${request.ownerUserId}`,
|
|
377
|
+
)}`
|
|
378
|
+
: `user #${request.ownerUserId}`;
|
|
379
|
+
const buildTitle = sanitizeSponsorTerminalLabel(
|
|
380
|
+
request.buildTitle,
|
|
381
|
+
`Build #${request.buildId}`,
|
|
382
|
+
);
|
|
383
|
+
printJsonOrLines(
|
|
384
|
+
options,
|
|
385
|
+
{
|
|
386
|
+
duty: publicDuty(state.duty),
|
|
387
|
+
teamAccessRequest: request,
|
|
388
|
+
assignment: null,
|
|
389
|
+
nextCommand: "lumine sponsor duty watch --json",
|
|
390
|
+
},
|
|
391
|
+
[
|
|
392
|
+
`${requester} asked to invite this sponsor account to ${buildTitle}, owned by ${owner}.`,
|
|
393
|
+
"Ask the sponsor whether they want to join. Their usual Twinkle team invitation is already waiting; no Workshop work starts unless they accept it.",
|
|
394
|
+
"Do not hold the Workshop queue open for a reply. Return to duty after sharing this notice.",
|
|
395
|
+
],
|
|
396
|
+
);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
353
399
|
if (claim.job) {
|
|
354
400
|
state = await recordClaim({ options, state, claim });
|
|
355
401
|
const prepared = await prepareClaimedJob({
|
package/package.json
CHANGED
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -93,6 +93,41 @@ Mikey; routine administrator runs still escalate suspected alternate accounts
|
|
|
93
93
|
and never auto-enforce. `accounts add` and `note set` still accept only an
|
|
94
94
|
existing **unbanned** bucket.
|
|
95
95
|
|
|
96
|
+
### Shared verified-email policies
|
|
97
|
+
|
|
98
|
+
A verified address proves that an account can use that mailbox; it does not
|
|
99
|
+
always prove that every account using the mailbox is one person. When a teacher,
|
|
100
|
+
school, or other provisioning owner intentionally verifies separate people's
|
|
101
|
+
accounts with one address, record that confirmed operational fact explicitly:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
lumine admin ai-email-policy get --email teacher@example.com --json
|
|
105
|
+
lumine admin ai-email-policy set --email teacher@example.com \
|
|
106
|
+
--mode separate_accounts \
|
|
107
|
+
--note "Teacher-confirmed classroom provisioning address" --json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`separate_accounts` preserves ordinary email verification and recovery while
|
|
111
|
+
giving every current and future account whose stable verified address matches
|
|
112
|
+
its own AI Energy and AI Card quota identity. The mutation re-attributes the
|
|
113
|
+
current day's per-account usage, updates all affected live quota projections
|
|
114
|
+
from canonical server state, and records the real operator in the private
|
|
115
|
+
Lumine audit log. It is a provisioning policy, not an alternate-account finding
|
|
116
|
+
or moderation flag, and must never be inferred from account count, usernames,
|
|
117
|
+
devices, IPs, or email-provider heuristics.
|
|
118
|
+
|
|
119
|
+
The CLI accepts a successful `set` only when the writer response confirms the
|
|
120
|
+
requested policy and reports every matching account on its exact expected
|
|
121
|
+
durable projection. A missing or mismatched projection fails closed instead of
|
|
122
|
+
letting an operator mistake a stored policy row for completed account repair.
|
|
123
|
+
|
|
124
|
+
Use `--mode automatic` with a new explanatory `--note` to restore Twinkle's
|
|
125
|
+
default verified-email grouping. Both modes are durable and apply to later
|
|
126
|
+
signups and later verifications without another code change. An active
|
|
127
|
+
email-wide manual identity rule conflicts with `separate_accounts`, so the API
|
|
128
|
+
rejects either operation until the operator explicitly resolves the conflict.
|
|
129
|
+
Exact per-user manual bucket rules remain authoritative in both modes.
|
|
130
|
+
|
|
96
131
|
### Audited identity inspection
|
|
97
132
|
|
|
98
133
|
Account-family evidence is private operator work, never a Zero/Ciel public
|