@lumi.ai/runner 0.15.12 → 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 +165 -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
|
/**
|
|
@@ -418,6 +429,17 @@ function autonomyRule(agent) {
|
|
|
418
429
|
}
|
|
419
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.";
|
|
420
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
|
+
|
|
421
443
|
// ../shared/dist/engineLimit.js
|
|
422
444
|
function isEngineLimited(limit3, now) {
|
|
423
445
|
return !!limit3 && limit3.resetsAt > now;
|
|
@@ -968,7 +990,7 @@ function mcpUrl(config2) {
|
|
|
968
990
|
}
|
|
969
991
|
|
|
970
992
|
// src/version.ts
|
|
971
|
-
var RUNNER_VERSION = true ? "0.15.
|
|
993
|
+
var RUNNER_VERSION = true ? "0.15.13" : "0.0.0-dev";
|
|
972
994
|
|
|
973
995
|
// src/auth.ts
|
|
974
996
|
import { signInWithCustomToken } from "firebase/auth";
|
|
@@ -2668,11 +2690,56 @@ async function loadRunnerSecrets(db, shipId) {
|
|
|
2668
2690
|
return snap.exists() ? snap.data() : null;
|
|
2669
2691
|
}
|
|
2670
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
|
+
|
|
2671
2738
|
// src/jobs/engineLimits.ts
|
|
2672
2739
|
import {
|
|
2673
|
-
collection as
|
|
2740
|
+
collection as collection5,
|
|
2674
2741
|
deleteDoc,
|
|
2675
|
-
doc as
|
|
2742
|
+
doc as doc6,
|
|
2676
2743
|
onSnapshot,
|
|
2677
2744
|
setDoc
|
|
2678
2745
|
} from "firebase/firestore";
|
|
@@ -2701,7 +2768,7 @@ function nextResetAt(limits, now) {
|
|
|
2701
2768
|
return soonest;
|
|
2702
2769
|
}
|
|
2703
2770
|
function limitRef(db, shipId, engineId) {
|
|
2704
|
-
return
|
|
2771
|
+
return doc6(db, COLLECTIONS.ships, shipId, COLLECTIONS.engineLimits, engineId);
|
|
2705
2772
|
}
|
|
2706
2773
|
async function noteEngineLimit(db, shipId, engineId, limit3, runnerId) {
|
|
2707
2774
|
await setDoc(limitRef(db, shipId, engineId), {
|
|
@@ -2718,7 +2785,7 @@ async function clearEngineLimit(db, shipId, engineId) {
|
|
|
2718
2785
|
}
|
|
2719
2786
|
function subscribeEngineLimits(db, shipId, cb, onError) {
|
|
2720
2787
|
return onSnapshot(
|
|
2721
|
-
|
|
2788
|
+
collection5(db, COLLECTIONS.ships, shipId, COLLECTIONS.engineLimits),
|
|
2722
2789
|
(snap) => cb(snap.docs.map((d) => ({ id: d.id, ...d.data() }))),
|
|
2723
2790
|
(e) => onError?.(e)
|
|
2724
2791
|
);
|
|
@@ -2798,11 +2865,11 @@ function selectDispatch(input) {
|
|
|
2798
2865
|
// src/jobs/finish.ts
|
|
2799
2866
|
import {
|
|
2800
2867
|
addDoc,
|
|
2801
|
-
collection as
|
|
2868
|
+
collection as collection6,
|
|
2802
2869
|
deleteField,
|
|
2803
|
-
doc as
|
|
2804
|
-
getDoc as
|
|
2805
|
-
getDocs as
|
|
2870
|
+
doc as doc7,
|
|
2871
|
+
getDoc as getDoc6,
|
|
2872
|
+
getDocs as getDocs5,
|
|
2806
2873
|
limit as fsLimit,
|
|
2807
2874
|
orderBy as orderBy3,
|
|
2808
2875
|
query as query3,
|
|
@@ -2847,10 +2914,10 @@ function backstopReportContent(resultText2) {
|
|
|
2847
2914
|
return { report: report4, summary };
|
|
2848
2915
|
}
|
|
2849
2916
|
async function finalizeJob(db, shipId, job, input) {
|
|
2850
|
-
const shipRef =
|
|
2851
|
-
const jobRef =
|
|
2917
|
+
const shipRef = doc7(db, COLLECTIONS.ships, shipId);
|
|
2918
|
+
const jobRef = doc7(shipRef, COLLECTIONS.jobs, job.id);
|
|
2852
2919
|
const now = Date.now();
|
|
2853
|
-
const usageRef =
|
|
2920
|
+
const usageRef = doc7(shipRef, COLLECTIONS.usageDaily, utcDay(now));
|
|
2854
2921
|
await runTransaction(db, async (tx) => {
|
|
2855
2922
|
const usageSnap = await tx.get(usageRef);
|
|
2856
2923
|
const jobSnap = await tx.get(jobRef);
|
|
@@ -2912,7 +2979,7 @@ async function finalizeJob(db, shipId, job, input) {
|
|
|
2912
2979
|
}
|
|
2913
2980
|
async function requeueForRetry(db, shipId, job, error) {
|
|
2914
2981
|
await runTransaction(db, async (tx) => {
|
|
2915
|
-
tx.update(
|
|
2982
|
+
tx.update(doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id), {
|
|
2916
2983
|
status: "queued",
|
|
2917
2984
|
attempt: job.attempt + 1,
|
|
2918
2985
|
error: error.slice(0, 1500),
|
|
@@ -2922,7 +2989,7 @@ async function requeueForRetry(db, shipId, job, error) {
|
|
|
2922
2989
|
});
|
|
2923
2990
|
}
|
|
2924
2991
|
async function releaseJob(db, shipId, job, reason) {
|
|
2925
|
-
await updateDoc(
|
|
2992
|
+
await updateDoc(doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id), {
|
|
2926
2993
|
status: "queued",
|
|
2927
2994
|
runnerId: "",
|
|
2928
2995
|
startedAt: 0,
|
|
@@ -2938,13 +3005,13 @@ async function releaseJob(db, shipId, job, reason) {
|
|
|
2938
3005
|
});
|
|
2939
3006
|
}
|
|
2940
3007
|
async function markTaskFailed(db, shipId, job, error, statuses) {
|
|
2941
|
-
const shipRef =
|
|
2942
|
-
const taskRef =
|
|
3008
|
+
const shipRef = doc7(db, COLLECTIONS.ships, shipId);
|
|
3009
|
+
const taskRef = doc7(shipRef, COLLECTIONS.tasks, job.taskId);
|
|
2943
3010
|
const now = Date.now();
|
|
2944
3011
|
const failedStatus = firstStatusIn(statuses, "failed")?.id ?? "failed";
|
|
2945
3012
|
await runTransaction(db, async (tx) => {
|
|
2946
3013
|
tx.update(taskRef, { status: failedStatus, updatedAt: now });
|
|
2947
|
-
tx.set(
|
|
3014
|
+
tx.set(doc7(collection6(taskRef, COLLECTIONS.activity)), {
|
|
2948
3015
|
author: { type: "agent", id: job.agentId },
|
|
2949
3016
|
createdAt: now,
|
|
2950
3017
|
kind: "comment",
|
|
@@ -2958,9 +3025,9 @@ ${error.slice(0, 800)}
|
|
|
2958
3025
|
});
|
|
2959
3026
|
}
|
|
2960
3027
|
async function markTaskStopped(db, shipId, job, stoppedBy) {
|
|
2961
|
-
const taskRef =
|
|
3028
|
+
const taskRef = doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.tasks, job.taskId);
|
|
2962
3029
|
const who = await actorName(db, shipId, stoppedBy);
|
|
2963
|
-
await addDoc(
|
|
3030
|
+
await addDoc(collection6(taskRef, COLLECTIONS.activity), {
|
|
2964
3031
|
author: { type: "agent", id: job.agentId },
|
|
2965
3032
|
createdAt: Date.now(),
|
|
2966
3033
|
kind: "comment",
|
|
@@ -2969,7 +3036,7 @@ async function markTaskStopped(db, shipId, job, stoppedBy) {
|
|
|
2969
3036
|
}
|
|
2970
3037
|
async function actorName(db, shipId, actorId) {
|
|
2971
3038
|
try {
|
|
2972
|
-
const snap = await
|
|
3039
|
+
const snap = await getDoc6(doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.members, actorId));
|
|
2973
3040
|
const m = snap.data();
|
|
2974
3041
|
return m?.displayName || m?.email || actorId;
|
|
2975
3042
|
} catch {
|
|
@@ -2977,10 +3044,10 @@ async function actorName(db, shipId, actorId) {
|
|
|
2977
3044
|
}
|
|
2978
3045
|
}
|
|
2979
3046
|
async function markChatStopped(db, shipId, job, stoppedBy) {
|
|
2980
|
-
const chatRef =
|
|
3047
|
+
const chatRef = doc7(db, COLLECTIONS.ships, shipId, COLLECTIONS.chats, job.chatId);
|
|
2981
3048
|
const who = await actorName(db, shipId, stoppedBy);
|
|
2982
3049
|
const content = `Stopped by ${who} before I finished. Write again to start a fresh run.`;
|
|
2983
|
-
await addDoc(
|
|
3050
|
+
await addDoc(collection6(chatRef, COLLECTIONS.chatMessages), {
|
|
2984
3051
|
author: { type: "agent", id: job.agentId },
|
|
2985
3052
|
content,
|
|
2986
3053
|
chars: content.length,
|
|
@@ -2988,8 +3055,8 @@ async function markChatStopped(db, shipId, job, stoppedBy) {
|
|
|
2988
3055
|
});
|
|
2989
3056
|
}
|
|
2990
3057
|
async function markChatFailed(db, shipId, job, error) {
|
|
2991
|
-
const shipRef =
|
|
2992
|
-
const chatRef =
|
|
3058
|
+
const shipRef = doc7(db, COLLECTIONS.ships, shipId);
|
|
3059
|
+
const chatRef = doc7(shipRef, COLLECTIONS.chats, job.chatId);
|
|
2993
3060
|
const now = Date.now();
|
|
2994
3061
|
const content = `I could not finish replying \u2014 the run failed after ${job.attempt} attempt(s).
|
|
2995
3062
|
|
|
@@ -2998,7 +3065,7 @@ ${error.slice(0, 500)}
|
|
|
2998
3065
|
\`\`\`
|
|
2999
3066
|
|
|
3000
3067
|
Write again to start a fresh run.`;
|
|
3001
|
-
await addDoc(
|
|
3068
|
+
await addDoc(collection6(chatRef, COLLECTIONS.chatMessages), {
|
|
3002
3069
|
author: { type: "agent", id: job.agentId },
|
|
3003
3070
|
content,
|
|
3004
3071
|
chars: content.length,
|
|
@@ -3016,9 +3083,9 @@ function backstopReplyContent(resultText2) {
|
|
|
3016
3083
|
return `${text.slice(0, MAX_CHAT_MESSAGE_CHARS - marker.length)}${marker}`;
|
|
3017
3084
|
}
|
|
3018
3085
|
async function ensureChatReply(db, shipId, job, resultText2) {
|
|
3019
|
-
const chatRef =
|
|
3020
|
-
const messagesCol =
|
|
3021
|
-
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(
|
|
3022
3089
|
query3(
|
|
3023
3090
|
messagesCol,
|
|
3024
3091
|
where3("createdAt", ">=", job.startedAt || 0),
|
|
@@ -3841,7 +3908,7 @@ async function startDaemon() {
|
|
|
3841
3908
|
const warnedUnapproved = /* @__PURE__ */ new Set();
|
|
3842
3909
|
const shipRunnerRef = (shipId) => {
|
|
3843
3910
|
const session = sess(shipId);
|
|
3844
|
-
return
|
|
3911
|
+
return doc8(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.runners, session.runnerId);
|
|
3845
3912
|
};
|
|
3846
3913
|
const needsRefill = /* @__PURE__ */ new Set();
|
|
3847
3914
|
let beating = false;
|
|
@@ -3852,7 +3919,7 @@ async function startDaemon() {
|
|
|
3852
3919
|
try {
|
|
3853
3920
|
for (const shipId of [...serving]) {
|
|
3854
3921
|
try {
|
|
3855
|
-
const snap = await
|
|
3922
|
+
const snap = await getDoc7(shipRunnerRef(shipId));
|
|
3856
3923
|
if (!snap.exists()) {
|
|
3857
3924
|
forgetShipLocally(shipId, "a captain removed this machine on the Daemons page");
|
|
3858
3925
|
continue;
|
|
@@ -3909,9 +3976,9 @@ async function startDaemon() {
|
|
|
3909
3976
|
needsRefill.delete(shipId);
|
|
3910
3977
|
if (!serving.has(shipId) || approved.get(shipId) !== true) continue;
|
|
3911
3978
|
try {
|
|
3912
|
-
const snap = await
|
|
3979
|
+
const snap = await getDocs6(
|
|
3913
3980
|
query4(
|
|
3914
|
-
|
|
3981
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs),
|
|
3915
3982
|
where4("status", "==", "queued"),
|
|
3916
3983
|
orderBy4("createdAt", "asc")
|
|
3917
3984
|
)
|
|
@@ -3943,7 +4010,7 @@ async function startDaemon() {
|
|
|
3943
4010
|
};
|
|
3944
4011
|
for (const shipId of serving) {
|
|
3945
4012
|
const q = query4(
|
|
3946
|
-
|
|
4013
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs),
|
|
3947
4014
|
where4("status", "==", "queued"),
|
|
3948
4015
|
orderBy4("createdAt", "asc")
|
|
3949
4016
|
);
|
|
@@ -3966,7 +4033,7 @@ async function startDaemon() {
|
|
|
3966
4033
|
),
|
|
3967
4034
|
// Agents, purely so the claim gate can resolve a queued job's engine without a read.
|
|
3968
4035
|
onSnapshot2(
|
|
3969
|
-
|
|
4036
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents),
|
|
3970
4037
|
(snap) => {
|
|
3971
4038
|
for (const d of snap.docs) {
|
|
3972
4039
|
agentEngines.set(`${shipId}/${d.id}`, agentEngine(d.data()));
|
|
@@ -3988,7 +4055,7 @@ async function startDaemon() {
|
|
|
3988
4055
|
* what happens. Testing is somebody's decision, and `doctor` is where it is made on purpose.
|
|
3989
4056
|
*/
|
|
3990
4057
|
onSnapshot2(
|
|
3991
|
-
|
|
4058
|
+
collection7(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.mcpServers),
|
|
3992
4059
|
(snap) => {
|
|
3993
4060
|
const servers = snap.docs.map(
|
|
3994
4061
|
(d) => ({ id: d.id, ...d.data() })
|
|
@@ -4197,7 +4264,7 @@ async function startDaemon() {
|
|
|
4197
4264
|
pending.set(`${shipId}/${job.id}`, { shipId, job });
|
|
4198
4265
|
return null;
|
|
4199
4266
|
}
|
|
4200
|
-
const jobRef =
|
|
4267
|
+
const jobRef = doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id);
|
|
4201
4268
|
try {
|
|
4202
4269
|
let claimed = null;
|
|
4203
4270
|
await runTransaction2(sess(shipId).fb.db, async (tx) => {
|
|
@@ -4220,7 +4287,7 @@ async function startDaemon() {
|
|
|
4220
4287
|
}
|
|
4221
4288
|
async function setAgentStatus(shipId, agentId, status) {
|
|
4222
4289
|
try {
|
|
4223
|
-
await updateDoc2(
|
|
4290
|
+
await updateDoc2(doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents, agentId), { status });
|
|
4224
4291
|
} catch (e) {
|
|
4225
4292
|
log2(`agent status update failed: ${e instanceof Error ? e.message : e}`);
|
|
4226
4293
|
}
|
|
@@ -4267,8 +4334,8 @@ async function startDaemon() {
|
|
|
4267
4334
|
void (async () => {
|
|
4268
4335
|
if (slot.stop) return;
|
|
4269
4336
|
try {
|
|
4270
|
-
const snap = await
|
|
4271
|
-
|
|
4337
|
+
const snap = await getDoc7(
|
|
4338
|
+
doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id)
|
|
4272
4339
|
);
|
|
4273
4340
|
const fresh = snap.data();
|
|
4274
4341
|
if (!fresh?.stopRequestedAt || slot.stop) return;
|
|
@@ -4308,7 +4375,7 @@ async function startDaemon() {
|
|
|
4308
4375
|
let statuses = DEFAULT_TASK_STATUSES;
|
|
4309
4376
|
let knownSecrets = [];
|
|
4310
4377
|
const progress = createProgressWriter({
|
|
4311
|
-
write: (p) => updateDoc2(
|
|
4378
|
+
write: (p) => updateDoc2(doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id), {
|
|
4312
4379
|
progress: p
|
|
4313
4380
|
}),
|
|
4314
4381
|
secrets: () => knownSecrets,
|
|
@@ -4322,8 +4389,8 @@ async function startDaemon() {
|
|
|
4322
4389
|
onDenied: () => {
|
|
4323
4390
|
void (async () => {
|
|
4324
4391
|
try {
|
|
4325
|
-
const snap = await
|
|
4326
|
-
|
|
4392
|
+
const snap = await getDoc7(
|
|
4393
|
+
doc8(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs, job.id)
|
|
4327
4394
|
);
|
|
4328
4395
|
const fresh = snap.data();
|
|
4329
4396
|
if (!fresh || fresh.status !== "running" || fresh.runnerId !== sess(shipId).runnerId) {
|
|
@@ -4338,7 +4405,7 @@ async function startDaemon() {
|
|
|
4338
4405
|
});
|
|
4339
4406
|
progress.push({ kind: "start", label: "Starting up" });
|
|
4340
4407
|
try {
|
|
4341
|
-
|
|
4408
|
+
let secrets = await loadSecrets(shipId);
|
|
4342
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 }) };
|
|
4343
4410
|
if (packed.kind === "task" && !isWorkflowSentinel(job.workflowId) && !packed.ctx.workflow) {
|
|
4344
4411
|
log2(
|
|
@@ -4349,6 +4416,17 @@ async function startDaemon() {
|
|
|
4349
4416
|
engineId = agentEngine(agent);
|
|
4350
4417
|
usage = { ...usage, engine: engineId };
|
|
4351
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;
|
|
4352
4430
|
const missing = missingSecretsFor(engineId, secrets);
|
|
4353
4431
|
if (missing.length > 0 && !process.env.CREW_CLAUDE_BIN) {
|
|
4354
4432
|
throw new Error(
|
|
@@ -5147,7 +5225,7 @@ function setParallel(config2, value, ship2) {
|
|
|
5147
5225
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5148
5226
|
import fs7 from "node:fs";
|
|
5149
5227
|
import path8 from "node:path";
|
|
5150
|
-
import { collection as
|
|
5228
|
+
import { collection as collection8, doc as doc9, getDoc as getDoc8, getDocs as getDocs7 } from "firebase/firestore";
|
|
5151
5229
|
|
|
5152
5230
|
// src/cli/session.ts
|
|
5153
5231
|
async function openShipSession(shipId) {
|
|
@@ -5316,8 +5394,8 @@ async function checkShips(config2) {
|
|
|
5316
5394
|
continue;
|
|
5317
5395
|
}
|
|
5318
5396
|
try {
|
|
5319
|
-
const snap = await
|
|
5320
|
-
|
|
5397
|
+
const snap = await getDoc8(
|
|
5398
|
+
doc9(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.runners, session.runnerId)
|
|
5321
5399
|
);
|
|
5322
5400
|
if (!snap.exists()) {
|
|
5323
5401
|
checks.push(
|
|
@@ -5358,7 +5436,7 @@ async function checkShips(config2) {
|
|
|
5358
5436
|
}
|
|
5359
5437
|
let agents = [];
|
|
5360
5438
|
try {
|
|
5361
|
-
const snap = await
|
|
5439
|
+
const snap = await getDocs7(collection8(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents));
|
|
5362
5440
|
agents = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
5363
5441
|
} catch {
|
|
5364
5442
|
}
|
|
@@ -5368,13 +5446,33 @@ async function checkShips(config2) {
|
|
|
5368
5446
|
if (agents.some((agent) => effectiveAgentTools(agent).github.enabled)) needsGithub = true;
|
|
5369
5447
|
try {
|
|
5370
5448
|
const secrets = await loadRunnerSecrets(session.fb.db, shipId);
|
|
5371
|
-
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
|
+
);
|
|
5372
5461
|
checks.push(
|
|
5373
|
-
missing.length
|
|
5462
|
+
missing.length > 0 ? fail(
|
|
5374
5463
|
`secrets:${shipId}`,
|
|
5375
5464
|
`Ship ${shipId} \u2014 credentials`,
|
|
5376
5465
|
`Missing: ${missing.join(", ")}.`,
|
|
5377
|
-
"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."
|
|
5378
5476
|
)
|
|
5379
5477
|
);
|
|
5380
5478
|
} catch (e) {
|
|
@@ -5388,8 +5486,8 @@ async function checkShips(config2) {
|
|
|
5388
5486
|
);
|
|
5389
5487
|
}
|
|
5390
5488
|
try {
|
|
5391
|
-
const snap = await
|
|
5392
|
-
|
|
5489
|
+
const snap = await getDocs7(
|
|
5490
|
+
collection8(session.fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.mcpServers)
|
|
5393
5491
|
);
|
|
5394
5492
|
const servers = snap.docs.map(
|
|
5395
5493
|
(d) => ({ id: d.id, ...d.data() })
|
|
@@ -5838,7 +5936,7 @@ async function runUninstall(options) {
|
|
|
5838
5936
|
}
|
|
5839
5937
|
|
|
5840
5938
|
// src/cli/commands/ship.ts
|
|
5841
|
-
import { doc as
|
|
5939
|
+
import { doc as doc10, getDoc as getDoc9 } from "firebase/firestore";
|
|
5842
5940
|
async function listMyShips() {
|
|
5843
5941
|
const config2 = requireConfig();
|
|
5844
5942
|
const ids = Object.keys(config2.shipKeys ?? {});
|
|
@@ -5846,7 +5944,7 @@ async function listMyShips() {
|
|
|
5846
5944
|
ids.map(async (id) => {
|
|
5847
5945
|
try {
|
|
5848
5946
|
const { fb } = await openShipSession(id);
|
|
5849
|
-
const snap = await
|
|
5947
|
+
const snap = await getDoc9(doc10(fb.db, COLLECTIONS.ships, id));
|
|
5850
5948
|
return { id, ...snap.data() };
|
|
5851
5949
|
} catch {
|
|
5852
5950
|
return null;
|
|
@@ -6006,11 +6104,11 @@ async function runSetup(options) {
|
|
|
6006
6104
|
|
|
6007
6105
|
// src/cli/commands/status.ts
|
|
6008
6106
|
import {
|
|
6009
|
-
collection as
|
|
6010
|
-
doc as
|
|
6107
|
+
collection as collection9,
|
|
6108
|
+
doc as doc11,
|
|
6011
6109
|
getCountFromServer as getCountFromServer2,
|
|
6012
|
-
getDoc as
|
|
6013
|
-
getDocs as
|
|
6110
|
+
getDoc as getDoc10,
|
|
6111
|
+
getDocs as getDocs8,
|
|
6014
6112
|
query as query5,
|
|
6015
6113
|
where as where5
|
|
6016
6114
|
} from "firebase/firestore";
|
|
@@ -6047,22 +6145,22 @@ async function runStatus() {
|
|
|
6047
6145
|
});
|
|
6048
6146
|
continue;
|
|
6049
6147
|
}
|
|
6050
|
-
const shipRef =
|
|
6051
|
-
const mirrorSnap = await
|
|
6148
|
+
const shipRef = doc11(session.fb.db, COLLECTIONS.ships, shipId);
|
|
6149
|
+
const mirrorSnap = await getDoc10(doc11(shipRef, COLLECTIONS.runners, session.runnerId));
|
|
6052
6150
|
const mirror = mirrorSnap.data();
|
|
6053
6151
|
let queued = 0;
|
|
6054
6152
|
try {
|
|
6055
6153
|
const counted = await getCountFromServer2(
|
|
6056
|
-
query5(
|
|
6154
|
+
query5(collection9(shipRef, COLLECTIONS.jobs), where5("status", "==", "queued"))
|
|
6057
6155
|
);
|
|
6058
6156
|
queued = counted.data().count;
|
|
6059
6157
|
} catch {
|
|
6060
6158
|
queued = 0;
|
|
6061
6159
|
}
|
|
6062
|
-
const usageSnap = await
|
|
6160
|
+
const usageSnap = await getDoc10(doc11(shipRef, COLLECTIONS.usageDaily, utcDay(Date.now())));
|
|
6063
6161
|
const today = { ...EMPTY_USAGE_TOTALS, ...usageSnap.data()?.totals ?? {} };
|
|
6064
6162
|
const now = Date.now();
|
|
6065
|
-
const limitsSnap = await
|
|
6163
|
+
const limitsSnap = await getDocs8(collection9(shipRef, COLLECTIONS.engineLimits));
|
|
6066
6164
|
const engineLimits = limitsSnap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((l) => isEngineLimited(l, now));
|
|
6067
6165
|
ships.push({
|
|
6068
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.",
|