@lumi.ai/runner 0.15.12 → 0.15.14
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/cli.js +194 -67
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,11 +6,11 @@ import { Command } from "commander";
|
|
|
6
6
|
// src/daemon.ts
|
|
7
7
|
import os4 from "node:os";
|
|
8
8
|
import {
|
|
9
|
-
collection as
|
|
9
|
+
collection as collection7,
|
|
10
10
|
deleteField as deleteField2,
|
|
11
|
-
doc as
|
|
12
|
-
getDoc as
|
|
13
|
-
getDocs as
|
|
11
|
+
doc as doc8,
|
|
12
|
+
getDoc as getDoc7,
|
|
13
|
+
getDocs as getDocs6,
|
|
14
14
|
onSnapshot as onSnapshot2,
|
|
15
15
|
orderBy as orderBy4,
|
|
16
16
|
query as query4,
|
|
@@ -277,6 +277,34 @@ var COLLECTIONS = {
|
|
|
277
277
|
/** ROOT collection (§15.44): one liveness document per relay instance, overwritten each
|
|
278
278
|
* report. Not a Ship subcollection — a relay serves every Ship and belongs to none. */
|
|
279
279
|
browserRelays: "browserRelays",
|
|
280
|
+
/**
|
|
281
|
+
* ROOT (§15.64): `crew_stripe_events/{eventId}` — the webhook idempotency ledger.
|
|
282
|
+
*
|
|
283
|
+
* Root rather than Ship-scoped because a Stripe event is addressed to an ACCOUNT, and the Ship
|
|
284
|
+
* it concerns is read out of its metadata AFTER the duplicate check. Scoping it under a Ship
|
|
285
|
+
* would mean resolving the Ship before knowing whether the event had already been handled,
|
|
286
|
+
* which is the wrong order — and would strand the ledger row when that Ship is purged.
|
|
287
|
+
*/
|
|
288
|
+
crewStripeEvents: "crewStripeEvents",
|
|
289
|
+
/**
|
|
290
|
+
* ROOT (§15.64): `crewBillingAccounts/{uid}` — one person's subscription, entire.
|
|
291
|
+
*
|
|
292
|
+
* Keyed by auth uid, not by Ship, because ONE PERSON HAS ONE SUBSCRIPTION covering every
|
|
293
|
+
* workspace they own. That is the whole of §15.64's second shape: a workspace is free, a seat is
|
|
294
|
+
* paid, and the seat count is pooled across the account — so the subscription cannot live on a
|
|
295
|
+
* Ship without being copied onto each of them, and N copies of one fact is N chances for a stale
|
|
296
|
+
* one.
|
|
297
|
+
*
|
|
298
|
+
* What a Ship still carries is only what `firestore.rules` and the triggers must read without a
|
|
299
|
+
* network call: `billing.status` and `billing.trialEnd`. Everything else — customer, subscription
|
|
300
|
+
* id, seats, interval, period end, the seat-sync error — is here, once.
|
|
301
|
+
*
|
|
302
|
+
* Deny-all to clients. Writable it is an account-takeover primitive (point your uid at somebody
|
|
303
|
+
* else's customer and the Portal session minted for you opens their card); readable it leaks who
|
|
304
|
+
* is a customer. The app never touches it — the Billing page gets its numbers from
|
|
305
|
+
* `crewBillingPreview`, which asks Stripe.
|
|
306
|
+
*/
|
|
307
|
+
crewBillingAccounts: "crewBillingAccounts",
|
|
280
308
|
/** `ships/{shipId}/notifications/{id}` — per-member in-app notifications. */
|
|
281
309
|
notifications: "notifications",
|
|
282
310
|
/** `ships/{shipId}/usage_daily/{yyyy-mm-dd}` — token-tracking aggregates. */
|
|
@@ -287,6 +315,17 @@ var COLLECTIONS = {
|
|
|
287
315
|
events: "events",
|
|
288
316
|
/** `ships/{shipId}/secrets/{docId}` — runner creds + MCP token hash (see secrets.ts). */
|
|
289
317
|
secrets: "secrets",
|
|
318
|
+
/**
|
|
319
|
+
* `ships/{shipId}/credentials/{credId}` — the AI accounts this crew thinks with (PRD §15.62;
|
|
320
|
+
* see credentials.ts). NON-SECRET metadata only — the token is `secrets/cred_{credId}`, which no
|
|
321
|
+
* rule releases to anybody but an enrolled runner. Same split as `mcp_servers`.
|
|
322
|
+
*
|
|
323
|
+
* A single lowercase word, which matters for the reason `mcp_servers` documents: the
|
|
324
|
+
* purge-completeness oracle finds Ship subcollections by regexing firestore.rules for
|
|
325
|
+
* `match /([a-z_]+)/{`, so a camelCase name would be invisible to the one test that exists to
|
|
326
|
+
* catch a forgotten credential-bearing collection.
|
|
327
|
+
*/
|
|
328
|
+
credentials: "credentials",
|
|
290
329
|
/** `ships/{shipId}/integrations/{docId}` — 3rd-party connectors (GitHub App; see github.ts). */
|
|
291
330
|
integrations: "integrations",
|
|
292
331
|
/**
|
|
@@ -418,6 +457,17 @@ function autonomyRule(agent) {
|
|
|
418
457
|
}
|
|
419
458
|
var ASK_GUIDANCE = "If the task says who should approve \u2014 in its description, or because a particular person owns that area \u2014 name them in the request's `ask` so it reaches them rather than everyone.";
|
|
420
459
|
|
|
460
|
+
// ../shared/dist/credentials.js
|
|
461
|
+
function resolveAgentCredential(agent, credentials, defaultEngine) {
|
|
462
|
+
const engine = agent.engine ?? defaultEngine;
|
|
463
|
+
if (agent.credentialId) {
|
|
464
|
+
return credentials.find((c) => c.id === agent.credentialId) ?? null;
|
|
465
|
+
}
|
|
466
|
+
const forEngine = credentials.filter((c) => c.engine === engine);
|
|
467
|
+
const oldest = [...forEngine].sort((a, b) => a.createdAt - b.createdAt)[0];
|
|
468
|
+
return oldest ?? void 0;
|
|
469
|
+
}
|
|
470
|
+
|
|
421
471
|
// ../shared/dist/engineLimit.js
|
|
422
472
|
function isEngineLimited(limit3, now) {
|
|
423
473
|
return !!limit3 && limit3.resetsAt > now;
|
|
@@ -839,6 +889,7 @@ var WEBHOOK_ACK_REASONS = {
|
|
|
839
889
|
duplicate: "Already delivered \u2014 this is a retry of a delivery that was accepted.",
|
|
840
890
|
noListener: "No enabled playbook on this Ship is listening to this hook.",
|
|
841
891
|
rateLimited: "Deliveries are arriving faster than one per second; this one was dropped.",
|
|
892
|
+
shipPaused: "This Ship's subscription is not active, so the delivery was accepted and no agent was woken.",
|
|
842
893
|
fanout: `Woke the first ${MAX_WEBHOOK_FANOUT} playbooks listening; the rest were skipped.`
|
|
843
894
|
};
|
|
844
895
|
|
|
@@ -968,7 +1019,7 @@ function mcpUrl(config2) {
|
|
|
968
1019
|
}
|
|
969
1020
|
|
|
970
1021
|
// src/version.ts
|
|
971
|
-
var RUNNER_VERSION = true ? "0.15.
|
|
1022
|
+
var RUNNER_VERSION = true ? "0.15.14" : "0.0.0-dev";
|
|
972
1023
|
|
|
973
1024
|
// src/auth.ts
|
|
974
1025
|
import { signInWithCustomToken } from "firebase/auth";
|
|
@@ -2668,11 +2719,56 @@ async function loadRunnerSecrets(db, shipId) {
|
|
|
2668
2719
|
return snap.exists() ? snap.data() : null;
|
|
2669
2720
|
}
|
|
2670
2721
|
|
|
2722
|
+
// src/jobs/credentials.ts
|
|
2723
|
+
import { collection as collection4, doc as doc5, getDoc as getDoc5, getDocs as getDocs4 } from "firebase/firestore";
|
|
2724
|
+
async function loadShipCredentials(db, shipId) {
|
|
2725
|
+
const snap = await withFirestoreRetry(
|
|
2726
|
+
() => getDocs4(collection4(db, COLLECTIONS.ships, shipId, COLLECTIONS.credentials))
|
|
2727
|
+
);
|
|
2728
|
+
return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
2729
|
+
}
|
|
2730
|
+
async function loadCredentialToken(db, shipId, credentialId) {
|
|
2731
|
+
const snap = await withFirestoreRetry(
|
|
2732
|
+
() => getDoc5(doc5(db, COLLECTIONS.ships, shipId, COLLECTIONS.secrets, credentialId))
|
|
2733
|
+
);
|
|
2734
|
+
if (!snap.exists()) return null;
|
|
2735
|
+
const token = snap.data().token;
|
|
2736
|
+
return typeof token === "string" && token.trim() ? token : null;
|
|
2737
|
+
}
|
|
2738
|
+
async function resolveJobSecrets(input) {
|
|
2739
|
+
const { agent, credentials, runnerSecrets } = input;
|
|
2740
|
+
const engineId = agentEngine(agent);
|
|
2741
|
+
const resolved = resolveAgentCredential(agent, credentials, DEFAULT_ENGINE_ID);
|
|
2742
|
+
if (resolved === void 0) return { secrets: runnerSecrets };
|
|
2743
|
+
if (resolved === null) {
|
|
2744
|
+
return {
|
|
2745
|
+
secrets: runnerSecrets,
|
|
2746
|
+
problem: `This agent is set to an AI credential that no longer exists on the Ship. Point it at another one in Settings \u203A AI credentials, or add the credential back.`
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
const token = await loadCredentialToken(input.db, input.shipId, resolved.id);
|
|
2750
|
+
if (!token) {
|
|
2751
|
+
return {
|
|
2752
|
+
secrets: runnerSecrets,
|
|
2753
|
+
problem: `The AI credential "${resolved.label}" has no token stored. Erase it in Settings \u203A AI credentials and add it again.`
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
const key = getEngine(engineId).requiredSecrets[0]?.key;
|
|
2757
|
+
if (!key) return { secrets: runnerSecrets };
|
|
2758
|
+
return {
|
|
2759
|
+
secrets: {
|
|
2760
|
+
...runnerSecrets ?? { updatedAt: resolved.updatedAt, updatedBy: resolved.createdBy },
|
|
2761
|
+
[key]: token,
|
|
2762
|
+
[`${key}Tail`]: resolved.tail
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2671
2767
|
// src/jobs/engineLimits.ts
|
|
2672
2768
|
import {
|
|
2673
|
-
collection as
|
|
2769
|
+
collection as collection5,
|
|
2674
2770
|
deleteDoc,
|
|
2675
|
-
doc as
|
|
2771
|
+
doc as doc6,
|
|
2676
2772
|
onSnapshot,
|
|
2677
2773
|
setDoc
|
|
2678
2774
|
} from "firebase/firestore";
|
|
@@ -2701,7 +2797,7 @@ function nextResetAt(limits, now) {
|
|
|
2701
2797
|
return soonest;
|
|
2702
2798
|
}
|
|
2703
2799
|
function limitRef(db, shipId, engineId) {
|
|
2704
|
-
return
|
|
2800
|
+
return doc6(db, COLLECTIONS.ships, shipId, COLLECTIONS.engineLimits, engineId);
|
|
2705
2801
|
}
|
|
2706
2802
|
async function noteEngineLimit(db, shipId, engineId, limit3, runnerId) {
|
|
2707
2803
|
await setDoc(limitRef(db, shipId, engineId), {
|
|
@@ -2718,7 +2814,7 @@ async function clearEngineLimit(db, shipId, engineId) {
|
|
|
2718
2814
|
}
|
|
2719
2815
|
function subscribeEngineLimits(db, shipId, cb, onError) {
|
|
2720
2816
|
return onSnapshot(
|
|
2721
|
-
|
|
2817
|
+
collection5(db, COLLECTIONS.ships, shipId, COLLECTIONS.engineLimits),
|
|
2722
2818
|
(snap) => cb(snap.docs.map((d) => ({ id: d.id, ...d.data() }))),
|
|
2723
2819
|
(e) => onError?.(e)
|
|
2724
2820
|
);
|
|
@@ -2798,11 +2894,11 @@ function selectDispatch(input) {
|
|
|
2798
2894
|
// src/jobs/finish.ts
|
|
2799
2895
|
import {
|
|
2800
2896
|
addDoc,
|
|
2801
|
-
collection as
|
|
2897
|
+
collection as collection6,
|
|
2802
2898
|
deleteField,
|
|
2803
|
-
doc as
|
|
2804
|
-
getDoc as
|
|
2805
|
-
getDocs as
|
|
2899
|
+
doc as doc7,
|
|
2900
|
+
getDoc as getDoc6,
|
|
2901
|
+
getDocs as getDocs5,
|
|
2806
2902
|
limit as fsLimit,
|
|
2807
2903
|
orderBy as orderBy3,
|
|
2808
2904
|
query as query3,
|
|
@@ -2847,10 +2943,10 @@ function backstopReportContent(resultText2) {
|
|
|
2847
2943
|
return { report: report4, summary };
|
|
2848
2944
|
}
|
|
2849
2945
|
async function finalizeJob(db, shipId, job, input) {
|
|
2850
|
-
const shipRef =
|
|
2851
|
-
const jobRef =
|
|
2946
|
+
const shipRef = doc7(db, COLLECTIONS.ships, shipId);
|
|
2947
|
+
const jobRef = doc7(shipRef, COLLECTIONS.jobs, job.id);
|
|
2852
2948
|
const now = Date.now();
|
|
2853
|
-
const usageRef =
|
|
2949
|
+
const usageRef = doc7(shipRef, COLLECTIONS.usageDaily, utcDay(now));
|
|
2854
2950
|
await runTransaction(db, async (tx) => {
|
|
2855
2951
|
const usageSnap = await tx.get(usageRef);
|
|
2856
2952
|
const jobSnap = await tx.get(jobRef);
|
|
@@ -2912,7 +3008,7 @@ async function finalizeJob(db, shipId, job, input) {
|
|
|
2912
3008
|
}
|
|
2913
3009
|
async function requeueForRetry(db, shipId, job, error) {
|
|
2914
3010
|
await runTransaction(db, async (tx) => {
|
|
2915
|
-
tx.update(
|
|
3011
|
+
tx.update(doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id), {
|
|
2916
3012
|
status: "queued",
|
|
2917
3013
|
attempt: job.attempt + 1,
|
|
2918
3014
|
error: error.slice(0, 1500),
|
|
@@ -2922,7 +3018,7 @@ async function requeueForRetry(db, shipId, job, error) {
|
|
|
2922
3018
|
});
|
|
2923
3019
|
}
|
|
2924
3020
|
async function releaseJob(db, shipId, job, reason) {
|
|
2925
|
-
await updateDoc(
|
|
3021
|
+
await updateDoc(doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id), {
|
|
2926
3022
|
status: "queued",
|
|
2927
3023
|
runnerId: "",
|
|
2928
3024
|
startedAt: 0,
|
|
@@ -2938,13 +3034,13 @@ async function releaseJob(db, shipId, job, reason) {
|
|
|
2938
3034
|
});
|
|
2939
3035
|
}
|
|
2940
3036
|
async function markTaskFailed(db, shipId, job, error, statuses) {
|
|
2941
|
-
const shipRef =
|
|
2942
|
-
const taskRef =
|
|
3037
|
+
const shipRef = doc7(db, COLLECTIONS.ships, shipId);
|
|
3038
|
+
const taskRef = doc7(shipRef, COLLECTIONS.tasks, job.taskId);
|
|
2943
3039
|
const now = Date.now();
|
|
2944
3040
|
const failedStatus = firstStatusIn(statuses, "failed")?.id ?? "failed";
|
|
2945
3041
|
await runTransaction(db, async (tx) => {
|
|
2946
3042
|
tx.update(taskRef, { status: failedStatus, updatedAt: now });
|
|
2947
|
-
tx.set(
|
|
3043
|
+
tx.set(doc7(collection6(taskRef, COLLECTIONS.activity)), {
|
|
2948
3044
|
author: { type: "agent", id: job.agentId },
|
|
2949
3045
|
createdAt: now,
|
|
2950
3046
|
kind: "comment",
|
|
@@ -2958,9 +3054,9 @@ ${error.slice(0, 800)}
|
|
|
2958
3054
|
});
|
|
2959
3055
|
}
|
|
2960
3056
|
async function markTaskStopped(db, shipId, job, stoppedBy) {
|
|
2961
|
-
const taskRef =
|
|
3057
|
+
const taskRef = doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.tasks, job.taskId);
|
|
2962
3058
|
const who = await actorName(db, shipId, stoppedBy);
|
|
2963
|
-
await addDoc(
|
|
3059
|
+
await addDoc(collection6(taskRef, COLLECTIONS.activity), {
|
|
2964
3060
|
author: { type: "agent", id: job.agentId },
|
|
2965
3061
|
createdAt: Date.now(),
|
|
2966
3062
|
kind: "comment",
|
|
@@ -2969,7 +3065,7 @@ async function markTaskStopped(db, shipId, job, stoppedBy) {
|
|
|
2969
3065
|
}
|
|
2970
3066
|
async function actorName(db, shipId, actorId) {
|
|
2971
3067
|
try {
|
|
2972
|
-
const snap = await
|
|
3068
|
+
const snap = await getDoc6(doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.members, actorId));
|
|
2973
3069
|
const m = snap.data();
|
|
2974
3070
|
return m?.displayName || m?.email || actorId;
|
|
2975
3071
|
} catch {
|
|
@@ -2977,10 +3073,10 @@ async function actorName(db, shipId, actorId) {
|
|
|
2977
3073
|
}
|
|
2978
3074
|
}
|
|
2979
3075
|
async function markChatStopped(db, shipId, job, stoppedBy) {
|
|
2980
|
-
const chatRef =
|
|
3076
|
+
const chatRef = doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.chats, job.chatId);
|
|
2981
3077
|
const who = await actorName(db, shipId, stoppedBy);
|
|
2982
3078
|
const content = `Stopped by ${who} before I finished. Write again to start a fresh run.`;
|
|
2983
|
-
await addDoc(
|
|
3079
|
+
await addDoc(collection6(chatRef, COLLECTIONS.chatMessages), {
|
|
2984
3080
|
author: { type: "agent", id: job.agentId },
|
|
2985
3081
|
content,
|
|
2986
3082
|
chars: content.length,
|
|
@@ -2988,8 +3084,8 @@ async function markChatStopped(db, shipId, job, stoppedBy) {
|
|
|
2988
3084
|
});
|
|
2989
3085
|
}
|
|
2990
3086
|
async function markChatFailed(db, shipId, job, error) {
|
|
2991
|
-
const shipRef =
|
|
2992
|
-
const chatRef =
|
|
3087
|
+
const shipRef = doc7(db, COLLECTIONS.ships, shipId);
|
|
3088
|
+
const chatRef = doc7(shipRef, COLLECTIONS.chats, job.chatId);
|
|
2993
3089
|
const now = Date.now();
|
|
2994
3090
|
const content = `I could not finish replying \u2014 the run failed after ${job.attempt} attempt(s).
|
|
2995
3091
|
|
|
@@ -2998,7 +3094,7 @@ ${error.slice(0, 500)}
|
|
|
2998
3094
|
\`\`\`
|
|
2999
3095
|
|
|
3000
3096
|
Write again to start a fresh run.`;
|
|
3001
|
-
await addDoc(
|
|
3097
|
+
await addDoc(collection6(chatRef, COLLECTIONS.chatMessages), {
|
|
3002
3098
|
author: { type: "agent", id: job.agentId },
|
|
3003
3099
|
content,
|
|
3004
3100
|
chars: content.length,
|
|
@@ -3016,9 +3112,9 @@ function backstopReplyContent(resultText2) {
|
|
|
3016
3112
|
return `${text.slice(0, MAX_CHAT_MESSAGE_CHARS - marker.length)}${marker}`;
|
|
3017
3113
|
}
|
|
3018
3114
|
async function ensureChatReply(db, shipId, job, resultText2) {
|
|
3019
|
-
const chatRef =
|
|
3020
|
-
const messagesCol =
|
|
3021
|
-
const snap = await
|
|
3115
|
+
const chatRef = doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.chats, job.chatId);
|
|
3116
|
+
const messagesCol = collection6(chatRef, COLLECTIONS.chatMessages);
|
|
3117
|
+
const snap = await getDocs5(
|
|
3022
3118
|
query3(
|
|
3023
3119
|
messagesCol,
|
|
3024
3120
|
where3("createdAt", ">=", job.startedAt || 0),
|
|
@@ -3841,7 +3937,7 @@ async function startDaemon() {
|
|
|
3841
3937
|
const warnedUnapproved = /* @__PURE__ */ new Set();
|
|
3842
3938
|
const shipRunnerRef = (shipId) => {
|
|
3843
3939
|
const session = sess(shipId);
|
|
3844
|
-
return
|
|
3940
|
+
return doc8(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.runners, session.runnerId);
|
|
3845
3941
|
};
|
|
3846
3942
|
const needsRefill = /* @__PURE__ */ new Set();
|
|
3847
3943
|
let beating = false;
|
|
@@ -3852,7 +3948,7 @@ async function startDaemon() {
|
|
|
3852
3948
|
try {
|
|
3853
3949
|
for (const shipId of [...serving]) {
|
|
3854
3950
|
try {
|
|
3855
|
-
const snap = await
|
|
3951
|
+
const snap = await getDoc7(shipRunnerRef(shipId));
|
|
3856
3952
|
if (!snap.exists()) {
|
|
3857
3953
|
forgetShipLocally(shipId, "a captain removed this machine on the Daemons page");
|
|
3858
3954
|
continue;
|
|
@@ -3909,9 +4005,9 @@ async function startDaemon() {
|
|
|
3909
4005
|
needsRefill.delete(shipId);
|
|
3910
4006
|
if (!serving.has(shipId) || approved.get(shipId) !== true) continue;
|
|
3911
4007
|
try {
|
|
3912
|
-
const snap = await
|
|
4008
|
+
const snap = await getDocs6(
|
|
3913
4009
|
query4(
|
|
3914
|
-
|
|
4010
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs),
|
|
3915
4011
|
where4("status", "==", "queued"),
|
|
3916
4012
|
orderBy4("createdAt", "asc")
|
|
3917
4013
|
)
|
|
@@ -3943,7 +4039,7 @@ async function startDaemon() {
|
|
|
3943
4039
|
};
|
|
3944
4040
|
for (const shipId of serving) {
|
|
3945
4041
|
const q = query4(
|
|
3946
|
-
|
|
4042
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs),
|
|
3947
4043
|
where4("status", "==", "queued"),
|
|
3948
4044
|
orderBy4("createdAt", "asc")
|
|
3949
4045
|
);
|
|
@@ -3966,7 +4062,7 @@ async function startDaemon() {
|
|
|
3966
4062
|
),
|
|
3967
4063
|
// Agents, purely so the claim gate can resolve a queued job's engine without a read.
|
|
3968
4064
|
onSnapshot2(
|
|
3969
|
-
|
|
4065
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents),
|
|
3970
4066
|
(snap) => {
|
|
3971
4067
|
for (const d of snap.docs) {
|
|
3972
4068
|
agentEngines.set(`${shipId}/${d.id}`, agentEngine(d.data()));
|
|
@@ -3988,7 +4084,7 @@ async function startDaemon() {
|
|
|
3988
4084
|
* what happens. Testing is somebody's decision, and `doctor` is where it is made on purpose.
|
|
3989
4085
|
*/
|
|
3990
4086
|
onSnapshot2(
|
|
3991
|
-
|
|
4087
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.mcpServers),
|
|
3992
4088
|
(snap) => {
|
|
3993
4089
|
const servers = snap.docs.map(
|
|
3994
4090
|
(d) => ({ id: d.id, ...d.data() })
|
|
@@ -4197,7 +4293,7 @@ async function startDaemon() {
|
|
|
4197
4293
|
pending.set(`${shipId}/${job.id}`, { shipId, job });
|
|
4198
4294
|
return null;
|
|
4199
4295
|
}
|
|
4200
|
-
const jobRef =
|
|
4296
|
+
const jobRef = doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id);
|
|
4201
4297
|
try {
|
|
4202
4298
|
let claimed = null;
|
|
4203
4299
|
await runTransaction2(sess(shipId).fb.db, async (tx) => {
|
|
@@ -4220,7 +4316,7 @@ async function startDaemon() {
|
|
|
4220
4316
|
}
|
|
4221
4317
|
async function setAgentStatus(shipId, agentId, status) {
|
|
4222
4318
|
try {
|
|
4223
|
-
await updateDoc2(
|
|
4319
|
+
await updateDoc2(doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents, agentId), { status });
|
|
4224
4320
|
} catch (e) {
|
|
4225
4321
|
log2(`agent status update failed: ${e instanceof Error ? e.message : e}`);
|
|
4226
4322
|
}
|
|
@@ -4267,8 +4363,8 @@ async function startDaemon() {
|
|
|
4267
4363
|
void (async () => {
|
|
4268
4364
|
if (slot.stop) return;
|
|
4269
4365
|
try {
|
|
4270
|
-
const snap = await
|
|
4271
|
-
|
|
4366
|
+
const snap = await getDoc7(
|
|
4367
|
+
doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id)
|
|
4272
4368
|
);
|
|
4273
4369
|
const fresh = snap.data();
|
|
4274
4370
|
if (!fresh?.stopRequestedAt || slot.stop) return;
|
|
@@ -4308,7 +4404,7 @@ async function startDaemon() {
|
|
|
4308
4404
|
let statuses = DEFAULT_TASK_STATUSES;
|
|
4309
4405
|
let knownSecrets = [];
|
|
4310
4406
|
const progress = createProgressWriter({
|
|
4311
|
-
write: (p) => updateDoc2(
|
|
4407
|
+
write: (p) => updateDoc2(doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id), {
|
|
4312
4408
|
progress: p
|
|
4313
4409
|
}),
|
|
4314
4410
|
secrets: () => knownSecrets,
|
|
@@ -4322,8 +4418,8 @@ async function startDaemon() {
|
|
|
4322
4418
|
onDenied: () => {
|
|
4323
4419
|
void (async () => {
|
|
4324
4420
|
try {
|
|
4325
|
-
const snap = await
|
|
4326
|
-
|
|
4421
|
+
const snap = await getDoc7(
|
|
4422
|
+
doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id)
|
|
4327
4423
|
);
|
|
4328
4424
|
const fresh = snap.data();
|
|
4329
4425
|
if (!fresh || fresh.status !== "running" || fresh.runnerId !== sess(shipId).runnerId) {
|
|
@@ -4338,7 +4434,7 @@ async function startDaemon() {
|
|
|
4338
4434
|
});
|
|
4339
4435
|
progress.push({ kind: "start", label: "Starting up" });
|
|
4340
4436
|
try {
|
|
4341
|
-
|
|
4437
|
+
let secrets = await loadSecrets(shipId);
|
|
4342
4438
|
const packed = target.kind === "chat" ? { kind: "chat", ctx: await loadChatContext(sess(shipId).fb.db, shipId, { ...job, chatId: target.chatId }) } : { kind: "task", ctx: await loadJobContext(sess(shipId).fb.db, shipId, { ...job, taskId: target.taskId }) };
|
|
4343
4439
|
if (packed.kind === "task" && !isWorkflowSentinel(job.workflowId) && !packed.ctx.workflow) {
|
|
4344
4440
|
log2(
|
|
@@ -4349,6 +4445,17 @@ async function startDaemon() {
|
|
|
4349
4445
|
engineId = agentEngine(agent);
|
|
4350
4446
|
usage = { ...usage, engine: engineId };
|
|
4351
4447
|
statuses = shipTaskStatuses(ship2);
|
|
4448
|
+
const resolvedSecrets = await resolveJobSecrets({
|
|
4449
|
+
db: sess(shipId).fb.db,
|
|
4450
|
+
shipId,
|
|
4451
|
+
agent,
|
|
4452
|
+
runnerSecrets: secrets,
|
|
4453
|
+
credentials: await loadShipCredentials(sess(shipId).fb.db, shipId).catch(() => [])
|
|
4454
|
+
});
|
|
4455
|
+
if (resolvedSecrets.problem) {
|
|
4456
|
+
throw new Error(resolvedSecrets.problem);
|
|
4457
|
+
}
|
|
4458
|
+
secrets = resolvedSecrets.secrets;
|
|
4352
4459
|
const missing = missingSecretsFor(engineId, secrets);
|
|
4353
4460
|
if (missing.length > 0 && !process.env.CREW_CLAUDE_BIN) {
|
|
4354
4461
|
throw new Error(
|
|
@@ -5147,7 +5254,7 @@ function setParallel(config2, value, ship2) {
|
|
|
5147
5254
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5148
5255
|
import fs7 from "node:fs";
|
|
5149
5256
|
import path8 from "node:path";
|
|
5150
|
-
import { collection as
|
|
5257
|
+
import { collection as collection8, doc as doc9, getDoc as getDoc8, getDocs as getDocs7 } from "firebase/firestore";
|
|
5151
5258
|
|
|
5152
5259
|
// src/cli/session.ts
|
|
5153
5260
|
async function openShipSession(shipId) {
|
|
@@ -5316,8 +5423,8 @@ async function checkShips(config2) {
|
|
|
5316
5423
|
continue;
|
|
5317
5424
|
}
|
|
5318
5425
|
try {
|
|
5319
|
-
const snap = await
|
|
5320
|
-
|
|
5426
|
+
const snap = await getDoc8(
|
|
5427
|
+
doc9(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.runners, session.runnerId)
|
|
5321
5428
|
);
|
|
5322
5429
|
if (!snap.exists()) {
|
|
5323
5430
|
checks.push(
|
|
@@ -5358,7 +5465,7 @@ async function checkShips(config2) {
|
|
|
5358
5465
|
}
|
|
5359
5466
|
let agents = [];
|
|
5360
5467
|
try {
|
|
5361
|
-
const snap = await
|
|
5468
|
+
const snap = await getDocs7(collection8(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents));
|
|
5362
5469
|
agents = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
5363
5470
|
} catch {
|
|
5364
5471
|
}
|
|
@@ -5368,13 +5475,33 @@ async function checkShips(config2) {
|
|
|
5368
5475
|
if (agents.some((agent) => effectiveAgentTools(agent).github.enabled)) needsGithub = true;
|
|
5369
5476
|
try {
|
|
5370
5477
|
const secrets = await loadRunnerSecrets(session.fb.db, shipId);
|
|
5371
|
-
const
|
|
5478
|
+
const credentials = await loadShipCredentials(session.fb.db, shipId).catch(() => []);
|
|
5479
|
+
const missing = [
|
|
5480
|
+
...new Set(
|
|
5481
|
+
[...shipEngines].flatMap((id) => {
|
|
5482
|
+
if (credentials.some((c) => c.engine === id)) return [];
|
|
5483
|
+
return missingSecretsFor(id, secrets);
|
|
5484
|
+
})
|
|
5485
|
+
)
|
|
5486
|
+
];
|
|
5487
|
+
const orphaned = agents.filter(
|
|
5488
|
+
(agent) => resolveAgentCredential(agent, credentials, DEFAULT_ENGINE_ID) === null
|
|
5489
|
+
);
|
|
5372
5490
|
checks.push(
|
|
5373
|
-
missing.length
|
|
5491
|
+
missing.length > 0 ? fail(
|
|
5374
5492
|
`secrets:${shipId}`,
|
|
5375
5493
|
`Ship ${shipId} \u2014 credentials`,
|
|
5376
5494
|
`Missing: ${missing.join(", ")}.`,
|
|
5377
|
-
"A captain saves these in Ship Settings \u2192
|
|
5495
|
+
"A captain saves these in Ship Settings \u2192 AI credentials."
|
|
5496
|
+
) : orphaned.length > 0 ? fail(
|
|
5497
|
+
`secrets:${shipId}`,
|
|
5498
|
+
`Ship ${shipId} \u2014 credentials`,
|
|
5499
|
+
`${orphaned.length} agent(s) are set to a credential that no longer exists: ${orphaned.map((a) => a.name).join(", ")}.`,
|
|
5500
|
+
"Point them at another one in Ship Settings \u2192 AI credentials."
|
|
5501
|
+
) : ok(
|
|
5502
|
+
`secrets:${shipId}`,
|
|
5503
|
+
`Ship ${shipId} \u2014 credentials`,
|
|
5504
|
+
"All required secrets are saved."
|
|
5378
5505
|
)
|
|
5379
5506
|
);
|
|
5380
5507
|
} catch (e) {
|
|
@@ -5388,8 +5515,8 @@ async function checkShips(config2) {
|
|
|
5388
5515
|
);
|
|
5389
5516
|
}
|
|
5390
5517
|
try {
|
|
5391
|
-
const snap = await
|
|
5392
|
-
|
|
5518
|
+
const snap = await getDocs7(
|
|
5519
|
+
collection8(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.mcpServers)
|
|
5393
5520
|
);
|
|
5394
5521
|
const servers = snap.docs.map(
|
|
5395
5522
|
(d) => ({ id: d.id, ...d.data() })
|
|
@@ -5838,7 +5965,7 @@ async function runUninstall(options) {
|
|
|
5838
5965
|
}
|
|
5839
5966
|
|
|
5840
5967
|
// src/cli/commands/ship.ts
|
|
5841
|
-
import { doc as
|
|
5968
|
+
import { doc as doc10, getDoc as getDoc9 } from "firebase/firestore";
|
|
5842
5969
|
async function listMyShips() {
|
|
5843
5970
|
const config2 = requireConfig();
|
|
5844
5971
|
const ids = Object.keys(config2.shipKeys ?? {});
|
|
@@ -5846,7 +5973,7 @@ async function listMyShips() {
|
|
|
5846
5973
|
ids.map(async (id) => {
|
|
5847
5974
|
try {
|
|
5848
5975
|
const { fb } = await openShipSession(id);
|
|
5849
|
-
const snap = await
|
|
5976
|
+
const snap = await getDoc9(doc10(fb.db, COLLECTIONS.ships, id));
|
|
5850
5977
|
return { id, ...snap.data() };
|
|
5851
5978
|
} catch {
|
|
5852
5979
|
return null;
|
|
@@ -6006,11 +6133,11 @@ async function runSetup(options) {
|
|
|
6006
6133
|
|
|
6007
6134
|
// src/cli/commands/status.ts
|
|
6008
6135
|
import {
|
|
6009
|
-
collection as
|
|
6010
|
-
doc as
|
|
6136
|
+
collection as collection9,
|
|
6137
|
+
doc as doc11,
|
|
6011
6138
|
getCountFromServer as getCountFromServer2,
|
|
6012
|
-
getDoc as
|
|
6013
|
-
getDocs as
|
|
6139
|
+
getDoc as getDoc10,
|
|
6140
|
+
getDocs as getDocs8,
|
|
6014
6141
|
query as query5,
|
|
6015
6142
|
where as where5
|
|
6016
6143
|
} from "firebase/firestore";
|
|
@@ -6047,22 +6174,22 @@ async function runStatus() {
|
|
|
6047
6174
|
});
|
|
6048
6175
|
continue;
|
|
6049
6176
|
}
|
|
6050
|
-
const shipRef =
|
|
6051
|
-
const mirrorSnap = await
|
|
6177
|
+
const shipRef = doc11(session.fb.db, COLLECTIONS.ships, shipId);
|
|
6178
|
+
const mirrorSnap = await getDoc10(doc11(shipRef, COLLECTIONS.runners, session.runnerId));
|
|
6052
6179
|
const mirror = mirrorSnap.data();
|
|
6053
6180
|
let queued = 0;
|
|
6054
6181
|
try {
|
|
6055
6182
|
const counted = await getCountFromServer2(
|
|
6056
|
-
query5(
|
|
6183
|
+
query5(collection9(shipRef, COLLECTIONS.jobs), where5("status", "==", "queued"))
|
|
6057
6184
|
);
|
|
6058
6185
|
queued = counted.data().count;
|
|
6059
6186
|
} catch {
|
|
6060
6187
|
queued = 0;
|
|
6061
6188
|
}
|
|
6062
|
-
const usageSnap = await
|
|
6189
|
+
const usageSnap = await getDoc10(doc11(shipRef, COLLECTIONS.usageDaily, utcDay(Date.now())));
|
|
6063
6190
|
const today = { ...EMPTY_USAGE_TOTALS, ...usageSnap.data()?.totals ?? {} };
|
|
6064
6191
|
const now = Date.now();
|
|
6065
|
-
const limitsSnap = await
|
|
6192
|
+
const limitsSnap = await getDocs8(collection9(shipRef, COLLECTIONS.engineLimits));
|
|
6066
6193
|
const engineLimits = limitsSnap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((l) => isEngineLimited(l, now));
|
|
6067
6194
|
ships.push({
|
|
6068
6195
|
shipId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumi.ai/runner",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lumi Crew runner daemon — claims jobs from your Ships and executes them as headless Claude sessions on your own machine.",
|
|
6
6
|
"//name": "The ONLY package in this monorepo published to the public registry, so it is the one that does not follow the internal @lumi/crew-* convention: `@lumi` is not a scope we own, `@lumi.ai` is (the npm org). The workspace DIRECTORY stays packages/crew/runner — renaming the package is not renaming the folder.",
|