@lumi.ai/runner 0.15.11 → 0.15.13
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 +193 -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,
|
|
@@ -287,6 +287,17 @@ var COLLECTIONS = {
|
|
|
287
287
|
events: "events",
|
|
288
288
|
/** `ships/{shipId}/secrets/{docId}` — runner creds + MCP token hash (see secrets.ts). */
|
|
289
289
|
secrets: "secrets",
|
|
290
|
+
/**
|
|
291
|
+
* `ships/{shipId}/credentials/{credId}` — the AI accounts this crew thinks with (PRD §15.62;
|
|
292
|
+
* see credentials.ts). NON-SECRET metadata only — the token is `secrets/cred_{credId}`, which no
|
|
293
|
+
* rule releases to anybody but an enrolled runner. Same split as `mcp_servers`.
|
|
294
|
+
*
|
|
295
|
+
* A single lowercase word, which matters for the reason `mcp_servers` documents: the
|
|
296
|
+
* purge-completeness oracle finds Ship subcollections by regexing firestore.rules for
|
|
297
|
+
* `match /([a-z_]+)/{`, so a camelCase name would be invisible to the one test that exists to
|
|
298
|
+
* catch a forgotten credential-bearing collection.
|
|
299
|
+
*/
|
|
300
|
+
credentials: "credentials",
|
|
290
301
|
/** `ships/{shipId}/integrations/{docId}` — 3rd-party connectors (GitHub App; see github.ts). */
|
|
291
302
|
integrations: "integrations",
|
|
292
303
|
/**
|
|
@@ -314,6 +325,24 @@ var COLLECTIONS = {
|
|
|
314
325
|
* MCP connection splits from `secrets/mcp_srv_{key}`.
|
|
315
326
|
*/
|
|
316
327
|
oauthGrants: "oauth_grants",
|
|
328
|
+
/**
|
|
329
|
+
* `ships/{shipId}/webhooks/{hookId}` — one inbound endpoint (PRD §15.56). NON-SECRET metadata;
|
|
330
|
+
* the signing secret is `secrets/webhook_{hookId}`, which no rule releases to anybody.
|
|
331
|
+
*
|
|
332
|
+
* SNAKE_CASE-safe as a single word, which matters for the reason `mcp_servers` documents: the
|
|
333
|
+
* purge-completeness oracle finds Ship subcollections by regexing firestore.rules for
|
|
334
|
+
* `match /([a-z_]+)/{`, so a camelCase name would be invisible to the one test that exists to
|
|
335
|
+
* catch a forgotten credential-bearing collection.
|
|
336
|
+
*/
|
|
337
|
+
webhooks: "webhooks",
|
|
338
|
+
/**
|
|
339
|
+
* `ships/{shipId}/webhook_deliveries/{key}` — the idempotency ledger (PRD §15.56).
|
|
340
|
+
*
|
|
341
|
+
* One document per accepted delivery, created transactionally: `tx.create` FAILING because the
|
|
342
|
+
* document exists IS the lock, so there is no read-then-write race for two concurrent retries to
|
|
343
|
+
* slip through. Swept by TTL rather than kept, since its only job is to be present.
|
|
344
|
+
*/
|
|
345
|
+
webhookDeliveries: "webhook_deliveries",
|
|
317
346
|
/** Top-level `users/{uid}/...` — per-user docs (runner machine registry). */
|
|
318
347
|
users: "users",
|
|
319
348
|
/** Top-level `githubInstallStates/{stateId}` — single-use install-flow nonces (backend only). */
|
|
@@ -400,6 +429,17 @@ function autonomyRule(agent) {
|
|
|
400
429
|
}
|
|
401
430
|
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.";
|
|
402
431
|
|
|
432
|
+
// ../shared/dist/credentials.js
|
|
433
|
+
function resolveAgentCredential(agent, credentials, defaultEngine) {
|
|
434
|
+
const engine = agent.engine ?? defaultEngine;
|
|
435
|
+
if (agent.credentialId) {
|
|
436
|
+
return credentials.find((c) => c.id === agent.credentialId) ?? null;
|
|
437
|
+
}
|
|
438
|
+
const forEngine = credentials.filter((c) => c.engine === engine);
|
|
439
|
+
const oldest = [...forEngine].sort((a, b) => a.createdAt - b.createdAt)[0];
|
|
440
|
+
return oldest ?? void 0;
|
|
441
|
+
}
|
|
442
|
+
|
|
403
443
|
// ../shared/dist/engineLimit.js
|
|
404
444
|
function isEngineLimited(limit3, now) {
|
|
405
445
|
return !!limit3 && limit3.resetsAt > now;
|
|
@@ -814,6 +854,16 @@ var EMPTY_USAGE_BY_AGENT = {
|
|
|
814
854
|
jobs: 0
|
|
815
855
|
};
|
|
816
856
|
|
|
857
|
+
// ../shared/dist/webhook.js
|
|
858
|
+
var MAX_WEBHOOK_FANOUT = 5;
|
|
859
|
+
var WEBHOOK_ACK_REASONS = {
|
|
860
|
+
disabled: "This hook is switched off, so the delivery was accepted and dropped.",
|
|
861
|
+
duplicate: "Already delivered \u2014 this is a retry of a delivery that was accepted.",
|
|
862
|
+
noListener: "No enabled playbook on this Ship is listening to this hook.",
|
|
863
|
+
rateLimited: "Deliveries are arriving faster than one per second; this one was dropped.",
|
|
864
|
+
fanout: `Woke the first ${MAX_WEBHOOK_FANOUT} playbooks listening; the rest were skipped.`
|
|
865
|
+
};
|
|
866
|
+
|
|
817
867
|
// ../shared/dist/workflow.js
|
|
818
868
|
var WORKFLOW_SENTINELS = ["generic", "assigned", "activity", "chat"];
|
|
819
869
|
function isWorkflowSentinel(id) {
|
|
@@ -940,7 +990,7 @@ function mcpUrl(config2) {
|
|
|
940
990
|
}
|
|
941
991
|
|
|
942
992
|
// src/version.ts
|
|
943
|
-
var RUNNER_VERSION = true ? "0.15.
|
|
993
|
+
var RUNNER_VERSION = true ? "0.15.13" : "0.0.0-dev";
|
|
944
994
|
|
|
945
995
|
// src/auth.ts
|
|
946
996
|
import { signInWithCustomToken } from "firebase/auth";
|
|
@@ -2640,11 +2690,56 @@ async function loadRunnerSecrets(db, shipId) {
|
|
|
2640
2690
|
return snap.exists() ? snap.data() : null;
|
|
2641
2691
|
}
|
|
2642
2692
|
|
|
2693
|
+
// src/jobs/credentials.ts
|
|
2694
|
+
import { collection as collection4, doc as doc5, getDoc as getDoc5, getDocs as getDocs4 } from "firebase/firestore";
|
|
2695
|
+
async function loadShipCredentials(db, shipId) {
|
|
2696
|
+
const snap = await withFirestoreRetry(
|
|
2697
|
+
() => getDocs4(collection4(db, COLLECTIONS.ships, shipId, COLLECTIONS.credentials))
|
|
2698
|
+
);
|
|
2699
|
+
return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
2700
|
+
}
|
|
2701
|
+
async function loadCredentialToken(db, shipId, credentialId) {
|
|
2702
|
+
const snap = await withFirestoreRetry(
|
|
2703
|
+
() => getDoc5(doc5(db, COLLECTIONS.ships, shipId, COLLECTIONS.secrets, credentialId))
|
|
2704
|
+
);
|
|
2705
|
+
if (!snap.exists()) return null;
|
|
2706
|
+
const token = snap.data().token;
|
|
2707
|
+
return typeof token === "string" && token.trim() ? token : null;
|
|
2708
|
+
}
|
|
2709
|
+
async function resolveJobSecrets(input) {
|
|
2710
|
+
const { agent, credentials, runnerSecrets } = input;
|
|
2711
|
+
const engineId = agentEngine(agent);
|
|
2712
|
+
const resolved = resolveAgentCredential(agent, credentials, DEFAULT_ENGINE_ID);
|
|
2713
|
+
if (resolved === void 0) return { secrets: runnerSecrets };
|
|
2714
|
+
if (resolved === null) {
|
|
2715
|
+
return {
|
|
2716
|
+
secrets: runnerSecrets,
|
|
2717
|
+
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.`
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
const token = await loadCredentialToken(input.db, input.shipId, resolved.id);
|
|
2721
|
+
if (!token) {
|
|
2722
|
+
return {
|
|
2723
|
+
secrets: runnerSecrets,
|
|
2724
|
+
problem: `The AI credential "${resolved.label}" has no token stored. Erase it in Settings \u203A AI credentials and add it again.`
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
const key = getEngine(engineId).requiredSecrets[0]?.key;
|
|
2728
|
+
if (!key) return { secrets: runnerSecrets };
|
|
2729
|
+
return {
|
|
2730
|
+
secrets: {
|
|
2731
|
+
...runnerSecrets ?? { updatedAt: resolved.updatedAt, updatedBy: resolved.createdBy },
|
|
2732
|
+
[key]: token,
|
|
2733
|
+
[`${key}Tail`]: resolved.tail
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2643
2738
|
// src/jobs/engineLimits.ts
|
|
2644
2739
|
import {
|
|
2645
|
-
collection as
|
|
2740
|
+
collection as collection5,
|
|
2646
2741
|
deleteDoc,
|
|
2647
|
-
doc as
|
|
2742
|
+
doc as doc6,
|
|
2648
2743
|
onSnapshot,
|
|
2649
2744
|
setDoc
|
|
2650
2745
|
} from "firebase/firestore";
|
|
@@ -2673,7 +2768,7 @@ function nextResetAt(limits, now) {
|
|
|
2673
2768
|
return soonest;
|
|
2674
2769
|
}
|
|
2675
2770
|
function limitRef(db, shipId, engineId) {
|
|
2676
|
-
return
|
|
2771
|
+
return doc6(db, COLLECTIONS.ships, shipId, COLLECTIONS.engineLimits, engineId);
|
|
2677
2772
|
}
|
|
2678
2773
|
async function noteEngineLimit(db, shipId, engineId, limit3, runnerId) {
|
|
2679
2774
|
await setDoc(limitRef(db, shipId, engineId), {
|
|
@@ -2690,7 +2785,7 @@ async function clearEngineLimit(db, shipId, engineId) {
|
|
|
2690
2785
|
}
|
|
2691
2786
|
function subscribeEngineLimits(db, shipId, cb, onError) {
|
|
2692
2787
|
return onSnapshot(
|
|
2693
|
-
|
|
2788
|
+
collection5(db, COLLECTIONS.ships, shipId, COLLECTIONS.engineLimits),
|
|
2694
2789
|
(snap) => cb(snap.docs.map((d) => ({ id: d.id, ...d.data() }))),
|
|
2695
2790
|
(e) => onError?.(e)
|
|
2696
2791
|
);
|
|
@@ -2770,11 +2865,11 @@ function selectDispatch(input) {
|
|
|
2770
2865
|
// src/jobs/finish.ts
|
|
2771
2866
|
import {
|
|
2772
2867
|
addDoc,
|
|
2773
|
-
collection as
|
|
2868
|
+
collection as collection6,
|
|
2774
2869
|
deleteField,
|
|
2775
|
-
doc as
|
|
2776
|
-
getDoc as
|
|
2777
|
-
getDocs as
|
|
2870
|
+
doc as doc7,
|
|
2871
|
+
getDoc as getDoc6,
|
|
2872
|
+
getDocs as getDocs5,
|
|
2778
2873
|
limit as fsLimit,
|
|
2779
2874
|
orderBy as orderBy3,
|
|
2780
2875
|
query as query3,
|
|
@@ -2819,10 +2914,10 @@ function backstopReportContent(resultText2) {
|
|
|
2819
2914
|
return { report: report4, summary };
|
|
2820
2915
|
}
|
|
2821
2916
|
async function finalizeJob(db, shipId, job, input) {
|
|
2822
|
-
const shipRef =
|
|
2823
|
-
const jobRef =
|
|
2917
|
+
const shipRef = doc7(db, COLLECTIONS.ships, shipId);
|
|
2918
|
+
const jobRef = doc7(shipRef, COLLECTIONS.jobs, job.id);
|
|
2824
2919
|
const now = Date.now();
|
|
2825
|
-
const usageRef =
|
|
2920
|
+
const usageRef = doc7(shipRef, COLLECTIONS.usageDaily, utcDay(now));
|
|
2826
2921
|
await runTransaction(db, async (tx) => {
|
|
2827
2922
|
const usageSnap = await tx.get(usageRef);
|
|
2828
2923
|
const jobSnap = await tx.get(jobRef);
|
|
@@ -2884,7 +2979,7 @@ async function finalizeJob(db, shipId, job, input) {
|
|
|
2884
2979
|
}
|
|
2885
2980
|
async function requeueForRetry(db, shipId, job, error) {
|
|
2886
2981
|
await runTransaction(db, async (tx) => {
|
|
2887
|
-
tx.update(
|
|
2982
|
+
tx.update(doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id), {
|
|
2888
2983
|
status: "queued",
|
|
2889
2984
|
attempt: job.attempt + 1,
|
|
2890
2985
|
error: error.slice(0, 1500),
|
|
@@ -2894,7 +2989,7 @@ async function requeueForRetry(db, shipId, job, error) {
|
|
|
2894
2989
|
});
|
|
2895
2990
|
}
|
|
2896
2991
|
async function releaseJob(db, shipId, job, reason) {
|
|
2897
|
-
await updateDoc(
|
|
2992
|
+
await updateDoc(doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id), {
|
|
2898
2993
|
status: "queued",
|
|
2899
2994
|
runnerId: "",
|
|
2900
2995
|
startedAt: 0,
|
|
@@ -2910,13 +3005,13 @@ async function releaseJob(db, shipId, job, reason) {
|
|
|
2910
3005
|
});
|
|
2911
3006
|
}
|
|
2912
3007
|
async function markTaskFailed(db, shipId, job, error, statuses) {
|
|
2913
|
-
const shipRef =
|
|
2914
|
-
const taskRef =
|
|
3008
|
+
const shipRef = doc7(db, COLLECTIONS.ships, shipId);
|
|
3009
|
+
const taskRef = doc7(shipRef, COLLECTIONS.tasks, job.taskId);
|
|
2915
3010
|
const now = Date.now();
|
|
2916
3011
|
const failedStatus = firstStatusIn(statuses, "failed")?.id ?? "failed";
|
|
2917
3012
|
await runTransaction(db, async (tx) => {
|
|
2918
3013
|
tx.update(taskRef, { status: failedStatus, updatedAt: now });
|
|
2919
|
-
tx.set(
|
|
3014
|
+
tx.set(doc7(collection6(taskRef, COLLECTIONS.activity)), {
|
|
2920
3015
|
author: { type: "agent", id: job.agentId },
|
|
2921
3016
|
createdAt: now,
|
|
2922
3017
|
kind: "comment",
|
|
@@ -2930,9 +3025,9 @@ ${error.slice(0, 800)}
|
|
|
2930
3025
|
});
|
|
2931
3026
|
}
|
|
2932
3027
|
async function markTaskStopped(db, shipId, job, stoppedBy) {
|
|
2933
|
-
const taskRef =
|
|
3028
|
+
const taskRef = doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.tasks, job.taskId);
|
|
2934
3029
|
const who = await actorName(db, shipId, stoppedBy);
|
|
2935
|
-
await addDoc(
|
|
3030
|
+
await addDoc(collection6(taskRef, COLLECTIONS.activity), {
|
|
2936
3031
|
author: { type: "agent", id: job.agentId },
|
|
2937
3032
|
createdAt: Date.now(),
|
|
2938
3033
|
kind: "comment",
|
|
@@ -2941,7 +3036,7 @@ async function markTaskStopped(db, shipId, job, stoppedBy) {
|
|
|
2941
3036
|
}
|
|
2942
3037
|
async function actorName(db, shipId, actorId) {
|
|
2943
3038
|
try {
|
|
2944
|
-
const snap = await
|
|
3039
|
+
const snap = await getDoc6(doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.members, actorId));
|
|
2945
3040
|
const m = snap.data();
|
|
2946
3041
|
return m?.displayName || m?.email || actorId;
|
|
2947
3042
|
} catch {
|
|
@@ -2949,10 +3044,10 @@ async function actorName(db, shipId, actorId) {
|
|
|
2949
3044
|
}
|
|
2950
3045
|
}
|
|
2951
3046
|
async function markChatStopped(db, shipId, job, stoppedBy) {
|
|
2952
|
-
const chatRef =
|
|
3047
|
+
const chatRef = doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.chats, job.chatId);
|
|
2953
3048
|
const who = await actorName(db, shipId, stoppedBy);
|
|
2954
3049
|
const content = `Stopped by ${who} before I finished. Write again to start a fresh run.`;
|
|
2955
|
-
await addDoc(
|
|
3050
|
+
await addDoc(collection6(chatRef, COLLECTIONS.chatMessages), {
|
|
2956
3051
|
author: { type: "agent", id: job.agentId },
|
|
2957
3052
|
content,
|
|
2958
3053
|
chars: content.length,
|
|
@@ -2960,8 +3055,8 @@ async function markChatStopped(db, shipId, job, stoppedBy) {
|
|
|
2960
3055
|
});
|
|
2961
3056
|
}
|
|
2962
3057
|
async function markChatFailed(db, shipId, job, error) {
|
|
2963
|
-
const shipRef =
|
|
2964
|
-
const chatRef =
|
|
3058
|
+
const shipRef = doc7(db, COLLECTIONS.ships, shipId);
|
|
3059
|
+
const chatRef = doc7(shipRef, COLLECTIONS.chats, job.chatId);
|
|
2965
3060
|
const now = Date.now();
|
|
2966
3061
|
const content = `I could not finish replying \u2014 the run failed after ${job.attempt} attempt(s).
|
|
2967
3062
|
|
|
@@ -2970,7 +3065,7 @@ ${error.slice(0, 500)}
|
|
|
2970
3065
|
\`\`\`
|
|
2971
3066
|
|
|
2972
3067
|
Write again to start a fresh run.`;
|
|
2973
|
-
await addDoc(
|
|
3068
|
+
await addDoc(collection6(chatRef, COLLECTIONS.chatMessages), {
|
|
2974
3069
|
author: { type: "agent", id: job.agentId },
|
|
2975
3070
|
content,
|
|
2976
3071
|
chars: content.length,
|
|
@@ -2988,9 +3083,9 @@ function backstopReplyContent(resultText2) {
|
|
|
2988
3083
|
return `${text.slice(0, MAX_CHAT_MESSAGE_CHARS - marker.length)}${marker}`;
|
|
2989
3084
|
}
|
|
2990
3085
|
async function ensureChatReply(db, shipId, job, resultText2) {
|
|
2991
|
-
const chatRef =
|
|
2992
|
-
const messagesCol =
|
|
2993
|
-
const snap = await
|
|
3086
|
+
const chatRef = doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.chats, job.chatId);
|
|
3087
|
+
const messagesCol = collection6(chatRef, COLLECTIONS.chatMessages);
|
|
3088
|
+
const snap = await getDocs5(
|
|
2994
3089
|
query3(
|
|
2995
3090
|
messagesCol,
|
|
2996
3091
|
where3("createdAt", ">=", job.startedAt || 0),
|
|
@@ -3813,7 +3908,7 @@ async function startDaemon() {
|
|
|
3813
3908
|
const warnedUnapproved = /* @__PURE__ */ new Set();
|
|
3814
3909
|
const shipRunnerRef = (shipId) => {
|
|
3815
3910
|
const session = sess(shipId);
|
|
3816
|
-
return
|
|
3911
|
+
return doc8(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.runners, session.runnerId);
|
|
3817
3912
|
};
|
|
3818
3913
|
const needsRefill = /* @__PURE__ */ new Set();
|
|
3819
3914
|
let beating = false;
|
|
@@ -3824,7 +3919,7 @@ async function startDaemon() {
|
|
|
3824
3919
|
try {
|
|
3825
3920
|
for (const shipId of [...serving]) {
|
|
3826
3921
|
try {
|
|
3827
|
-
const snap = await
|
|
3922
|
+
const snap = await getDoc7(shipRunnerRef(shipId));
|
|
3828
3923
|
if (!snap.exists()) {
|
|
3829
3924
|
forgetShipLocally(shipId, "a captain removed this machine on the Daemons page");
|
|
3830
3925
|
continue;
|
|
@@ -3881,9 +3976,9 @@ async function startDaemon() {
|
|
|
3881
3976
|
needsRefill.delete(shipId);
|
|
3882
3977
|
if (!serving.has(shipId) || approved.get(shipId) !== true) continue;
|
|
3883
3978
|
try {
|
|
3884
|
-
const snap = await
|
|
3979
|
+
const snap = await getDocs6(
|
|
3885
3980
|
query4(
|
|
3886
|
-
|
|
3981
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs),
|
|
3887
3982
|
where4("status", "==", "queued"),
|
|
3888
3983
|
orderBy4("createdAt", "asc")
|
|
3889
3984
|
)
|
|
@@ -3915,7 +4010,7 @@ async function startDaemon() {
|
|
|
3915
4010
|
};
|
|
3916
4011
|
for (const shipId of serving) {
|
|
3917
4012
|
const q = query4(
|
|
3918
|
-
|
|
4013
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs),
|
|
3919
4014
|
where4("status", "==", "queued"),
|
|
3920
4015
|
orderBy4("createdAt", "asc")
|
|
3921
4016
|
);
|
|
@@ -3938,7 +4033,7 @@ async function startDaemon() {
|
|
|
3938
4033
|
),
|
|
3939
4034
|
// Agents, purely so the claim gate can resolve a queued job's engine without a read.
|
|
3940
4035
|
onSnapshot2(
|
|
3941
|
-
|
|
4036
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents),
|
|
3942
4037
|
(snap) => {
|
|
3943
4038
|
for (const d of snap.docs) {
|
|
3944
4039
|
agentEngines.set(`${shipId}/${d.id}`, agentEngine(d.data()));
|
|
@@ -3960,7 +4055,7 @@ async function startDaemon() {
|
|
|
3960
4055
|
* what happens. Testing is somebody's decision, and `doctor` is where it is made on purpose.
|
|
3961
4056
|
*/
|
|
3962
4057
|
onSnapshot2(
|
|
3963
|
-
|
|
4058
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.mcpServers),
|
|
3964
4059
|
(snap) => {
|
|
3965
4060
|
const servers = snap.docs.map(
|
|
3966
4061
|
(d) => ({ id: d.id, ...d.data() })
|
|
@@ -4169,7 +4264,7 @@ async function startDaemon() {
|
|
|
4169
4264
|
pending.set(`${shipId}/${job.id}`, { shipId, job });
|
|
4170
4265
|
return null;
|
|
4171
4266
|
}
|
|
4172
|
-
const jobRef =
|
|
4267
|
+
const jobRef = doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id);
|
|
4173
4268
|
try {
|
|
4174
4269
|
let claimed = null;
|
|
4175
4270
|
await runTransaction2(sess(shipId).fb.db, async (tx) => {
|
|
@@ -4192,7 +4287,7 @@ async function startDaemon() {
|
|
|
4192
4287
|
}
|
|
4193
4288
|
async function setAgentStatus(shipId, agentId, status) {
|
|
4194
4289
|
try {
|
|
4195
|
-
await updateDoc2(
|
|
4290
|
+
await updateDoc2(doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents, agentId), { status });
|
|
4196
4291
|
} catch (e) {
|
|
4197
4292
|
log2(`agent status update failed: ${e instanceof Error ? e.message : e}`);
|
|
4198
4293
|
}
|
|
@@ -4239,8 +4334,8 @@ async function startDaemon() {
|
|
|
4239
4334
|
void (async () => {
|
|
4240
4335
|
if (slot.stop) return;
|
|
4241
4336
|
try {
|
|
4242
|
-
const snap = await
|
|
4243
|
-
|
|
4337
|
+
const snap = await getDoc7(
|
|
4338
|
+
doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id)
|
|
4244
4339
|
);
|
|
4245
4340
|
const fresh = snap.data();
|
|
4246
4341
|
if (!fresh?.stopRequestedAt || slot.stop) return;
|
|
@@ -4280,7 +4375,7 @@ async function startDaemon() {
|
|
|
4280
4375
|
let statuses = DEFAULT_TASK_STATUSES;
|
|
4281
4376
|
let knownSecrets = [];
|
|
4282
4377
|
const progress = createProgressWriter({
|
|
4283
|
-
write: (p) => updateDoc2(
|
|
4378
|
+
write: (p) => updateDoc2(doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id), {
|
|
4284
4379
|
progress: p
|
|
4285
4380
|
}),
|
|
4286
4381
|
secrets: () => knownSecrets,
|
|
@@ -4294,8 +4389,8 @@ async function startDaemon() {
|
|
|
4294
4389
|
onDenied: () => {
|
|
4295
4390
|
void (async () => {
|
|
4296
4391
|
try {
|
|
4297
|
-
const snap = await
|
|
4298
|
-
|
|
4392
|
+
const snap = await getDoc7(
|
|
4393
|
+
doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id)
|
|
4299
4394
|
);
|
|
4300
4395
|
const fresh = snap.data();
|
|
4301
4396
|
if (!fresh || fresh.status !== "running" || fresh.runnerId !== sess(shipId).runnerId) {
|
|
@@ -4310,7 +4405,7 @@ async function startDaemon() {
|
|
|
4310
4405
|
});
|
|
4311
4406
|
progress.push({ kind: "start", label: "Starting up" });
|
|
4312
4407
|
try {
|
|
4313
|
-
|
|
4408
|
+
let secrets = await loadSecrets(shipId);
|
|
4314
4409
|
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 }) };
|
|
4315
4410
|
if (packed.kind === "task" && !isWorkflowSentinel(job.workflowId) && !packed.ctx.workflow) {
|
|
4316
4411
|
log2(
|
|
@@ -4321,6 +4416,17 @@ async function startDaemon() {
|
|
|
4321
4416
|
engineId = agentEngine(agent);
|
|
4322
4417
|
usage = { ...usage, engine: engineId };
|
|
4323
4418
|
statuses = shipTaskStatuses(ship2);
|
|
4419
|
+
const resolvedSecrets = await resolveJobSecrets({
|
|
4420
|
+
db: sess(shipId).fb.db,
|
|
4421
|
+
shipId,
|
|
4422
|
+
agent,
|
|
4423
|
+
runnerSecrets: secrets,
|
|
4424
|
+
credentials: await loadShipCredentials(sess(shipId).fb.db, shipId).catch(() => [])
|
|
4425
|
+
});
|
|
4426
|
+
if (resolvedSecrets.problem) {
|
|
4427
|
+
throw new Error(resolvedSecrets.problem);
|
|
4428
|
+
}
|
|
4429
|
+
secrets = resolvedSecrets.secrets;
|
|
4324
4430
|
const missing = missingSecretsFor(engineId, secrets);
|
|
4325
4431
|
if (missing.length > 0 && !process.env.CREW_CLAUDE_BIN) {
|
|
4326
4432
|
throw new Error(
|
|
@@ -5119,7 +5225,7 @@ function setParallel(config2, value, ship2) {
|
|
|
5119
5225
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5120
5226
|
import fs7 from "node:fs";
|
|
5121
5227
|
import path8 from "node:path";
|
|
5122
|
-
import { collection as
|
|
5228
|
+
import { collection as collection8, doc as doc9, getDoc as getDoc8, getDocs as getDocs7 } from "firebase/firestore";
|
|
5123
5229
|
|
|
5124
5230
|
// src/cli/session.ts
|
|
5125
5231
|
async function openShipSession(shipId) {
|
|
@@ -5288,8 +5394,8 @@ async function checkShips(config2) {
|
|
|
5288
5394
|
continue;
|
|
5289
5395
|
}
|
|
5290
5396
|
try {
|
|
5291
|
-
const snap = await
|
|
5292
|
-
|
|
5397
|
+
const snap = await getDoc8(
|
|
5398
|
+
doc9(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.runners, session.runnerId)
|
|
5293
5399
|
);
|
|
5294
5400
|
if (!snap.exists()) {
|
|
5295
5401
|
checks.push(
|
|
@@ -5330,7 +5436,7 @@ async function checkShips(config2) {
|
|
|
5330
5436
|
}
|
|
5331
5437
|
let agents = [];
|
|
5332
5438
|
try {
|
|
5333
|
-
const snap = await
|
|
5439
|
+
const snap = await getDocs7(collection8(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents));
|
|
5334
5440
|
agents = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
5335
5441
|
} catch {
|
|
5336
5442
|
}
|
|
@@ -5340,13 +5446,33 @@ async function checkShips(config2) {
|
|
|
5340
5446
|
if (agents.some((agent) => effectiveAgentTools(agent).github.enabled)) needsGithub = true;
|
|
5341
5447
|
try {
|
|
5342
5448
|
const secrets = await loadRunnerSecrets(session.fb.db, shipId);
|
|
5343
|
-
const
|
|
5449
|
+
const credentials = await loadShipCredentials(session.fb.db, shipId).catch(() => []);
|
|
5450
|
+
const missing = [
|
|
5451
|
+
...new Set(
|
|
5452
|
+
[...shipEngines].flatMap((id) => {
|
|
5453
|
+
if (credentials.some((c) => c.engine === id)) return [];
|
|
5454
|
+
return missingSecretsFor(id, secrets);
|
|
5455
|
+
})
|
|
5456
|
+
)
|
|
5457
|
+
];
|
|
5458
|
+
const orphaned = agents.filter(
|
|
5459
|
+
(agent) => resolveAgentCredential(agent, credentials, DEFAULT_ENGINE_ID) === null
|
|
5460
|
+
);
|
|
5344
5461
|
checks.push(
|
|
5345
|
-
missing.length
|
|
5462
|
+
missing.length > 0 ? fail(
|
|
5346
5463
|
`secrets:${shipId}`,
|
|
5347
5464
|
`Ship ${shipId} \u2014 credentials`,
|
|
5348
5465
|
`Missing: ${missing.join(", ")}.`,
|
|
5349
|
-
"A captain saves these in Ship Settings \u2192
|
|
5466
|
+
"A captain saves these in Ship Settings \u2192 AI credentials."
|
|
5467
|
+
) : orphaned.length > 0 ? fail(
|
|
5468
|
+
`secrets:${shipId}`,
|
|
5469
|
+
`Ship ${shipId} \u2014 credentials`,
|
|
5470
|
+
`${orphaned.length} agent(s) are set to a credential that no longer exists: ${orphaned.map((a) => a.name).join(", ")}.`,
|
|
5471
|
+
"Point them at another one in Ship Settings \u2192 AI credentials."
|
|
5472
|
+
) : ok(
|
|
5473
|
+
`secrets:${shipId}`,
|
|
5474
|
+
`Ship ${shipId} \u2014 credentials`,
|
|
5475
|
+
"All required secrets are saved."
|
|
5350
5476
|
)
|
|
5351
5477
|
);
|
|
5352
5478
|
} catch (e) {
|
|
@@ -5360,8 +5486,8 @@ async function checkShips(config2) {
|
|
|
5360
5486
|
);
|
|
5361
5487
|
}
|
|
5362
5488
|
try {
|
|
5363
|
-
const snap = await
|
|
5364
|
-
|
|
5489
|
+
const snap = await getDocs7(
|
|
5490
|
+
collection8(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.mcpServers)
|
|
5365
5491
|
);
|
|
5366
5492
|
const servers = snap.docs.map(
|
|
5367
5493
|
(d) => ({ id: d.id, ...d.data() })
|
|
@@ -5810,7 +5936,7 @@ async function runUninstall(options) {
|
|
|
5810
5936
|
}
|
|
5811
5937
|
|
|
5812
5938
|
// src/cli/commands/ship.ts
|
|
5813
|
-
import { doc as
|
|
5939
|
+
import { doc as doc10, getDoc as getDoc9 } from "firebase/firestore";
|
|
5814
5940
|
async function listMyShips() {
|
|
5815
5941
|
const config2 = requireConfig();
|
|
5816
5942
|
const ids = Object.keys(config2.shipKeys ?? {});
|
|
@@ -5818,7 +5944,7 @@ async function listMyShips() {
|
|
|
5818
5944
|
ids.map(async (id) => {
|
|
5819
5945
|
try {
|
|
5820
5946
|
const { fb } = await openShipSession(id);
|
|
5821
|
-
const snap = await
|
|
5947
|
+
const snap = await getDoc9(doc10(fb.db, COLLECTIONS.ships, id));
|
|
5822
5948
|
return { id, ...snap.data() };
|
|
5823
5949
|
} catch {
|
|
5824
5950
|
return null;
|
|
@@ -5978,11 +6104,11 @@ async function runSetup(options) {
|
|
|
5978
6104
|
|
|
5979
6105
|
// src/cli/commands/status.ts
|
|
5980
6106
|
import {
|
|
5981
|
-
collection as
|
|
5982
|
-
doc as
|
|
6107
|
+
collection as collection9,
|
|
6108
|
+
doc as doc11,
|
|
5983
6109
|
getCountFromServer as getCountFromServer2,
|
|
5984
|
-
getDoc as
|
|
5985
|
-
getDocs as
|
|
6110
|
+
getDoc as getDoc10,
|
|
6111
|
+
getDocs as getDocs8,
|
|
5986
6112
|
query as query5,
|
|
5987
6113
|
where as where5
|
|
5988
6114
|
} from "firebase/firestore";
|
|
@@ -6019,22 +6145,22 @@ async function runStatus() {
|
|
|
6019
6145
|
});
|
|
6020
6146
|
continue;
|
|
6021
6147
|
}
|
|
6022
|
-
const shipRef =
|
|
6023
|
-
const mirrorSnap = await
|
|
6148
|
+
const shipRef = doc11(session.fb.db, COLLECTIONS.ships, shipId);
|
|
6149
|
+
const mirrorSnap = await getDoc10(doc11(shipRef, COLLECTIONS.runners, session.runnerId));
|
|
6024
6150
|
const mirror = mirrorSnap.data();
|
|
6025
6151
|
let queued = 0;
|
|
6026
6152
|
try {
|
|
6027
6153
|
const counted = await getCountFromServer2(
|
|
6028
|
-
query5(
|
|
6154
|
+
query5(collection9(shipRef, COLLECTIONS.jobs), where5("status", "==", "queued"))
|
|
6029
6155
|
);
|
|
6030
6156
|
queued = counted.data().count;
|
|
6031
6157
|
} catch {
|
|
6032
6158
|
queued = 0;
|
|
6033
6159
|
}
|
|
6034
|
-
const usageSnap = await
|
|
6160
|
+
const usageSnap = await getDoc10(doc11(shipRef, COLLECTIONS.usageDaily, utcDay(Date.now())));
|
|
6035
6161
|
const today = { ...EMPTY_USAGE_TOTALS, ...usageSnap.data()?.totals ?? {} };
|
|
6036
6162
|
const now = Date.now();
|
|
6037
|
-
const limitsSnap = await
|
|
6163
|
+
const limitsSnap = await getDocs8(collection9(shipRef, COLLECTIONS.engineLimits));
|
|
6038
6164
|
const engineLimits = limitsSnap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((l) => isEngineLimited(l, now));
|
|
6039
6165
|
ships.push({
|
|
6040
6166
|
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.13",
|
|
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.",
|