@edgehero/pi-dispatch-admin 1.1.0 → 1.3.0
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/index.mjs +251 -19
- package/package.json +1 -1
- package/skills/operate-pi-dispatch/SKILL.md +30 -0
package/dist/index.mjs
CHANGED
|
@@ -7611,7 +7611,7 @@ function isForgeKind(kind) {
|
|
|
7611
7611
|
function forgeSpec(kind) {
|
|
7612
7612
|
return isForgeKind(kind) ? FORGES[kind] : void 0;
|
|
7613
7613
|
}
|
|
7614
|
-
var FORGES, FORGE_KINDS, RUN_KINDS, MINTED_TOKEN_VARS;
|
|
7614
|
+
var FORGES, FORGE_KINDS, RUN_KINDS, MINTED_TOKEN_VARS, FORGE_HOST_VARS;
|
|
7615
7615
|
var init_forges = __esm({
|
|
7616
7616
|
"../worker/src/forges.mjs"() {
|
|
7617
7617
|
FORGES = {
|
|
@@ -7677,6 +7677,13 @@ var init_forges = __esm({
|
|
|
7677
7677
|
FORGE_KINDS = Object.freeze(Object.keys(FORGES));
|
|
7678
7678
|
RUN_KINDS = Object.freeze(["local", ...FORGE_KINDS]);
|
|
7679
7679
|
MINTED_TOKEN_VARS = new Set(FORGE_KINDS.flatMap((kind) => FORGES[kind].tokenVars));
|
|
7680
|
+
FORGE_HOST_VARS = new Set(FORGE_KINDS.map((kind) => FORGES[kind].hostVar).filter((name) => typeof name === "string"));
|
|
7681
|
+
}
|
|
7682
|
+
});
|
|
7683
|
+
|
|
7684
|
+
// ../worker/src/secret-profiles.mjs
|
|
7685
|
+
var init_secret_profiles = __esm({
|
|
7686
|
+
"../worker/src/secret-profiles.mjs"() {
|
|
7680
7687
|
}
|
|
7681
7688
|
});
|
|
7682
7689
|
|
|
@@ -7701,13 +7708,16 @@ function defaultGraphDir(env = process.env) {
|
|
|
7701
7708
|
function defaultSettingsFile() {
|
|
7702
7709
|
return `${process.env.TMPDIR ?? process.env.TEMP ?? "/tmp"}/pi-dispatch/settings.json`.replace(/\\/g, "/");
|
|
7703
7710
|
}
|
|
7704
|
-
var CHAIN_DEPTH_MAX_DEFAULT, CHAIN_MAX_PER_JOB_DEFAULT;
|
|
7711
|
+
var CHAIN_DEPTH_MAX_DEFAULT, CHAIN_MAX_PER_JOB_DEFAULT, WORKER_ONLY_SECRET_VARS, EGRESS_ENV_VARS;
|
|
7705
7712
|
var init_config2 = __esm({
|
|
7706
7713
|
"../worker/src/config.mjs"() {
|
|
7707
7714
|
init_egress();
|
|
7708
7715
|
init_forges();
|
|
7716
|
+
init_secret_profiles();
|
|
7709
7717
|
CHAIN_DEPTH_MAX_DEFAULT = 1;
|
|
7710
7718
|
CHAIN_MAX_PER_JOB_DEFAULT = 2;
|
|
7719
|
+
WORKER_ONLY_SECRET_VARS = /* @__PURE__ */ new Set(["GITHUB_APP_PRIVATE_KEY"]);
|
|
7720
|
+
EGRESS_ENV_VARS = /* @__PURE__ */ new Set(["HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "NODE_USE_ENV_PROXY"]);
|
|
7711
7721
|
}
|
|
7712
7722
|
});
|
|
7713
7723
|
|
|
@@ -7752,6 +7762,21 @@ function validateOverlay(candidate, log) {
|
|
|
7752
7762
|
if (!isIntInRange(value, 1, 10)) return { invalid: "concurrency must be an integer 1-10" };
|
|
7753
7763
|
overlay[key] = value;
|
|
7754
7764
|
break;
|
|
7765
|
+
case "secretProfiles": {
|
|
7766
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
7767
|
+
return { invalid: "secretProfiles must be an object mapping profile names to resolver paths" };
|
|
7768
|
+
}
|
|
7769
|
+
for (const name of Object.keys(value)) {
|
|
7770
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name)) {
|
|
7771
|
+
return { invalid: "secretProfiles names may use letters, digits, dot, dash and underscore only" };
|
|
7772
|
+
}
|
|
7773
|
+
if (typeof value[name] !== "string" || value[name].trim() === "") {
|
|
7774
|
+
return { invalid: `secretProfiles.${name} must be a non-empty absolute path` };
|
|
7775
|
+
}
|
|
7776
|
+
}
|
|
7777
|
+
overlay[key] = { ...value };
|
|
7778
|
+
break;
|
|
7779
|
+
}
|
|
7755
7780
|
case "softHoldPct":
|
|
7756
7781
|
if (!isIntInRange(value, 1, 99)) return { invalid: "softHoldPct must be an integer 1-99" };
|
|
7757
7782
|
overlay[key] = value;
|
|
@@ -7861,6 +7886,29 @@ var init_budget = __esm({
|
|
|
7861
7886
|
}
|
|
7862
7887
|
});
|
|
7863
7888
|
|
|
7889
|
+
// ../worker/src/reserved-env.mjs
|
|
7890
|
+
var CONTAINER_ENV_NAMES;
|
|
7891
|
+
var init_reserved_env = __esm({
|
|
7892
|
+
"../worker/src/reserved-env.mjs"() {
|
|
7893
|
+
CONTAINER_ENV_NAMES = /* @__PURE__ */ new Set([
|
|
7894
|
+
"PI_PROVIDER",
|
|
7895
|
+
"PI_MODEL",
|
|
7896
|
+
"PI_MAX_TURNS",
|
|
7897
|
+
"PI_MAX_TOKENS",
|
|
7898
|
+
"PI_JOB_ID",
|
|
7899
|
+
"PI_GLOBAL_ALLOW_EXTENSIONS",
|
|
7900
|
+
"PI_PACKAGES",
|
|
7901
|
+
"PI_SESSION_FILE",
|
|
7902
|
+
"PI_FLOW",
|
|
7903
|
+
"PI_COMMAND",
|
|
7904
|
+
"PI_OFFLINE",
|
|
7905
|
+
"PLAYWRIGHT_BROWSERS_PATH",
|
|
7906
|
+
"PLAYWRIGHT_MCP_BROWSER",
|
|
7907
|
+
"PLAYWRIGHT_MCP_SANDBOX"
|
|
7908
|
+
]);
|
|
7909
|
+
}
|
|
7910
|
+
});
|
|
7911
|
+
|
|
7864
7912
|
// ../worker/src/triggers.mjs
|
|
7865
7913
|
function isNonEmptyString2(value) {
|
|
7866
7914
|
return typeof value === "string" && value.trim() !== "";
|
|
@@ -7953,9 +8001,11 @@ function normalizeCron(on, run, index, path, state) {
|
|
|
7953
8001
|
validateInstructions(run, `cron trigger "${id}"`, path, { cron: true });
|
|
7954
8002
|
const resume = validateResumeFlag(run, `cron trigger "${id}"`, path);
|
|
7955
8003
|
validateReplicas(run, `cron trigger "${id}"`, path);
|
|
8004
|
+
const secrets = validateSecrets(run, `cron trigger "${id}"`, path);
|
|
8005
|
+
const secretsProfile = validateSecretsProfile(run, `cron trigger "${id}"`, path);
|
|
7956
8006
|
return {
|
|
7957
8007
|
on: { type: "cron", id, pattern },
|
|
7958
|
-
run: { kind: "local", folder: run.folder, flow: run.flow, task: run.task, provider: run.provider, model: run.model, maxTurns: run.maxTurns, github: run.github, packages, image, resume, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir } }
|
|
8008
|
+
run: { kind: "local", folder: run.folder, flow: run.flow, task: run.task, provider: run.provider, model: run.model, maxTurns: run.maxTurns, github: run.github, packages, image, resume, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile } }
|
|
7959
8009
|
};
|
|
7960
8010
|
}
|
|
7961
8011
|
function validatePackagesFlag(run, at, path) {
|
|
@@ -8086,6 +8136,56 @@ function validateReplicas(run, at, path) {
|
|
|
8086
8136
|
}
|
|
8087
8137
|
return replicas;
|
|
8088
8138
|
}
|
|
8139
|
+
function validateSecrets(run, at, path) {
|
|
8140
|
+
const secrets = run.secrets;
|
|
8141
|
+
if (secrets === void 0) return void 0;
|
|
8142
|
+
if (secrets === null || typeof secrets !== "object" || Array.isArray(secrets)) {
|
|
8143
|
+
throw configError(`${at}: run.secrets must be an object mapping environment variable names to references when present (got ${JSON.stringify(secrets)}): ${path}`);
|
|
8144
|
+
}
|
|
8145
|
+
const names = Object.keys(secrets);
|
|
8146
|
+
if (names.length === 0) {
|
|
8147
|
+
throw configError(`${at}: run.secrets is empty -- an empty map is a field that does nothing, and this trigger reads as though it binds a secret: ${path}`);
|
|
8148
|
+
}
|
|
8149
|
+
if (names.length > SECRETS_MAX) {
|
|
8150
|
+
throw configError(`${at}: run.secrets names ${names.length} variables, over the ${SECRETS_MAX} cap -- each one is resolved before the container starts, holding a concurrency slot while it runs: ${path}`);
|
|
8151
|
+
}
|
|
8152
|
+
for (const name of names) {
|
|
8153
|
+
if (!ENV_NAME.test(name)) {
|
|
8154
|
+
throw configError(`${at}: run.secrets key ${JSON.stringify(name)} is not an environment variable name (letters, digits and underscore, not starting with a digit): ${path}`);
|
|
8155
|
+
}
|
|
8156
|
+
if (RESERVED_ENV_NAMES.has(name)) {
|
|
8157
|
+
throw configError(`${at}: run.secrets key ${JSON.stringify(name)} is a variable the worker sets itself -- the job container would receive the worker's value, not this trigger's, and the trigger would look like it worked: ${path}`);
|
|
8158
|
+
}
|
|
8159
|
+
const reference = secrets[name];
|
|
8160
|
+
if (!isNonEmptyString2(reference)) {
|
|
8161
|
+
throw configError(`${at}: run.secrets.${name} must be a non-empty string reference for your resolver to read (got ${JSON.stringify(reference)}): ${path}`);
|
|
8162
|
+
}
|
|
8163
|
+
if (reference !== reference.trim()) {
|
|
8164
|
+
throw configError(`${at}: run.secrets.${name} must not have leading or trailing whitespace -- the file is the reviewed artifact and silently trimming it would make the file disagree with what the resolver is asked for: ${path}`);
|
|
8165
|
+
}
|
|
8166
|
+
if (reference.startsWith("-")) {
|
|
8167
|
+
throw configError(`${at}: run.secrets.${name} must not start with "-" -- it is passed as the resolver's first argument, where a leading dash parses as a flag: ${path}`);
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
if (run.resume === true) {
|
|
8171
|
+
throw configError(`${at}: run.secrets and run.resume cannot be combined -- a resumed job replays a transcript kept on host disk, and any command the agent ran that echoed a resolved value wrote it into that transcript, which is then prefilled into every later job on the same key: ${path}`);
|
|
8172
|
+
}
|
|
8173
|
+
return { ...secrets };
|
|
8174
|
+
}
|
|
8175
|
+
function validateSecretsProfile(run, at, path) {
|
|
8176
|
+
const profile = run.secretsProfile;
|
|
8177
|
+
if (profile === void 0) return void 0;
|
|
8178
|
+
if (!isNonEmptyString2(profile)) {
|
|
8179
|
+
throw configError(`${at}: run.secretsProfile must be a non-empty string naming one of the resolver profiles this deployment declares (got ${JSON.stringify(profile)}): ${path}`);
|
|
8180
|
+
}
|
|
8181
|
+
if (!ID_CHARSET.test(profile)) {
|
|
8182
|
+
throw configError(`${at}: run.secretsProfile ${JSON.stringify(profile)} may use letters, digits, dot, dash and underscore only -- PI_SECRET_PROFILES is a comma-separated list of name:path pairs, so a name carrying either separator cannot be declared: ${path}`);
|
|
8183
|
+
}
|
|
8184
|
+
if (run.secrets === void 0) {
|
|
8185
|
+
throw configError(`${at}: run.secretsProfile is set but run.secrets names nothing -- a profile that resolves no references is a field that does nothing: ${path}`);
|
|
8186
|
+
}
|
|
8187
|
+
return profile;
|
|
8188
|
+
}
|
|
8089
8189
|
function normalizeLabel(on, run, index, path) {
|
|
8090
8190
|
const at = `trigger at index ${index}`;
|
|
8091
8191
|
const predicate = validatePredicate(on, index, path, true);
|
|
@@ -8100,9 +8200,11 @@ function normalizeLabel(on, run, index, path) {
|
|
|
8100
8200
|
const resume = validateResumeFlag(run, at, path);
|
|
8101
8201
|
const repository = validateRepository(run, "label", at, path);
|
|
8102
8202
|
const replicas = validateReplicas(run, at, path);
|
|
8203
|
+
const secrets = validateSecrets(run, at, path);
|
|
8204
|
+
const secretsProfile = validateSecretsProfile(run, at, path);
|
|
8103
8205
|
return {
|
|
8104
8206
|
on: { type: "label", any: predicate.any, all: predicate.all, none: predicate.none },
|
|
8105
|
-
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository } }
|
|
8207
|
+
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile } }
|
|
8106
8208
|
};
|
|
8107
8209
|
}
|
|
8108
8210
|
function normalizeComment(on, run, index, path, state) {
|
|
@@ -8125,9 +8227,11 @@ function normalizeComment(on, run, index, path, state) {
|
|
|
8125
8227
|
const resume = validateResumeFlag(run, at, path);
|
|
8126
8228
|
const repository = validateRepository(run, "comment", at, path);
|
|
8127
8229
|
const replicas = validateReplicas(run, at, path);
|
|
8230
|
+
const secrets = validateSecrets(run, at, path);
|
|
8231
|
+
const secretsProfile = validateSecretsProfile(run, at, path);
|
|
8128
8232
|
return {
|
|
8129
8233
|
on: { type: "comment", phrase: on.phrase },
|
|
8130
|
-
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository } }
|
|
8234
|
+
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile } }
|
|
8131
8235
|
};
|
|
8132
8236
|
}
|
|
8133
8237
|
function normalizePullRequest(on, run, index, path) {
|
|
@@ -8161,6 +8265,8 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8161
8265
|
const resume = validateResumeFlag(run, at, path);
|
|
8162
8266
|
validateRepository(run, "pull_request", at, path);
|
|
8163
8267
|
const replicas = validateReplicas(run, at, path);
|
|
8268
|
+
const secrets = validateSecrets(run, at, path);
|
|
8269
|
+
const secretsProfile = validateSecretsProfile(run, at, path);
|
|
8164
8270
|
return {
|
|
8165
8271
|
on: {
|
|
8166
8272
|
type: "pull_request",
|
|
@@ -8172,7 +8278,7 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8172
8278
|
all: predicate.all,
|
|
8173
8279
|
none: predicate.none
|
|
8174
8280
|
},
|
|
8175
|
-
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions } }
|
|
8281
|
+
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile } }
|
|
8176
8282
|
};
|
|
8177
8283
|
}
|
|
8178
8284
|
function validateReviewState(on, actions, run, at, path) {
|
|
@@ -8194,11 +8300,12 @@ function validateReviewState(on, actions, run, at, path) {
|
|
|
8194
8300
|
}
|
|
8195
8301
|
return [...on.reviewState];
|
|
8196
8302
|
}
|
|
8197
|
-
var ON_TYPES, PR_ACTIONS, REVIEW_STATES, REVIEW_ACTION, ID_CHARSET, REPLICAS_MAX, INSTRUCTIONS_MAX;
|
|
8303
|
+
var ON_TYPES, PR_ACTIONS, REVIEW_STATES, REVIEW_ACTION, ID_CHARSET, REPLICAS_MAX, SECRETS_MAX, ENV_NAME, RESERVED_ENV_NAMES, INSTRUCTIONS_MAX;
|
|
8198
8304
|
var init_triggers = __esm({
|
|
8199
8305
|
"../worker/src/triggers.mjs"() {
|
|
8200
8306
|
init_config2();
|
|
8201
8307
|
init_forges();
|
|
8308
|
+
init_reserved_env();
|
|
8202
8309
|
ON_TYPES = /* @__PURE__ */ new Set(["cron", "label", "comment", "pull_request"]);
|
|
8203
8310
|
PR_ACTIONS = {
|
|
8204
8311
|
github: /* @__PURE__ */ new Set(["labeled", "opened", "synchronize", "reopened", "review_submitted"]),
|
|
@@ -8221,6 +8328,9 @@ var init_triggers = __esm({
|
|
|
8221
8328
|
REVIEW_ACTION = "review_submitted";
|
|
8222
8329
|
ID_CHARSET = /^[A-Za-z0-9._-]+$/;
|
|
8223
8330
|
REPLICAS_MAX = 3;
|
|
8331
|
+
SECRETS_MAX = 16;
|
|
8332
|
+
ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8333
|
+
RESERVED_ENV_NAMES = /* @__PURE__ */ new Set([...MINTED_TOKEN_VARS, ...FORGE_HOST_VARS, ...WORKER_ONLY_SECRET_VARS, ...EGRESS_ENV_VARS, ...CONTAINER_ENV_NAMES]);
|
|
8224
8334
|
INSTRUCTIONS_MAX = 2e3;
|
|
8225
8335
|
}
|
|
8226
8336
|
});
|
|
@@ -8553,7 +8663,7 @@ import { Queue } from "bullmq";
|
|
|
8553
8663
|
function makeQueue(connection) {
|
|
8554
8664
|
return new Queue(QUEUE, { connection });
|
|
8555
8665
|
}
|
|
8556
|
-
async function enqueueLocalJob(queue, { folder, flow, task, command, provider, model, maxTurns, image, skillsDir, chainDepth, parentJobId, jobId, now = /* @__PURE__ */ new Date() }) {
|
|
8666
|
+
async function enqueueLocalJob(queue, { folder, flow, task, command, provider, model, maxTurns, image, skillsDir, secrets, secretsProfile, chainDepth, parentJobId, jobId, now = /* @__PURE__ */ new Date() }) {
|
|
8557
8667
|
const minute = now.toISOString().slice(0, 16);
|
|
8558
8668
|
const id = jobId ?? localJobId({ folder, flow: command !== void 0 ? `cmd:${command}` : flow, task, minute });
|
|
8559
8669
|
const data = {
|
|
@@ -8574,6 +8684,11 @@ async function enqueueLocalJob(queue, { folder, flow, task, command, provider, m
|
|
|
8574
8684
|
// than inside `trigger` because a worker-host path is an execution knob, not a fact about the
|
|
8575
8685
|
// delivery -- and `trigger` is the object copied into /job/event.json.
|
|
8576
8686
|
...skillsDir !== void 0 && { skillsDir },
|
|
8687
|
+
// REQ-TRIGGER-SECRETS, on the forge path's terms: references only, resolved by the worker at job
|
|
8688
|
+
// start. A cron trigger may bind secrets (a nightly deploy is the obvious user), which is where
|
|
8689
|
+
// this differs from `replicas` -- that one is refused on a local job and this one is not.
|
|
8690
|
+
...secrets !== void 0 && { secrets },
|
|
8691
|
+
...secretsProfile !== void 0 && { secretsProfile },
|
|
8577
8692
|
...chainDepth !== void 0 && { chainDepth },
|
|
8578
8693
|
...parentJobId !== void 0 && { parentJobId }
|
|
8579
8694
|
};
|
|
@@ -10393,6 +10508,8 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
10393
10508
|
const resume = run.resume === true;
|
|
10394
10509
|
const forge = typeof run.kind === "string" && run.kind.trim() !== "" ? run.kind : null;
|
|
10395
10510
|
const replicas = Number.isInteger(run.replicas) && run.replicas > 1 ? run.replicas : null;
|
|
10511
|
+
const secrets = run.secrets !== null && typeof run.secrets === "object" && !Array.isArray(run.secrets) ? Object.keys(run.secrets).length : 0;
|
|
10512
|
+
const secretsProfile = typeof run.secretsProfile === "string" && run.secretsProfile.trim() !== "" ? run.secretsProfile : null;
|
|
10396
10513
|
switch (on.type) {
|
|
10397
10514
|
case "cron":
|
|
10398
10515
|
return {
|
|
@@ -10409,12 +10526,14 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
10409
10526
|
image,
|
|
10410
10527
|
skillsDir,
|
|
10411
10528
|
instructions,
|
|
10412
|
-
resume
|
|
10529
|
+
resume,
|
|
10530
|
+
secrets,
|
|
10531
|
+
secretsProfile
|
|
10413
10532
|
};
|
|
10414
10533
|
case "label":
|
|
10415
|
-
return { type: "label", any: normalizeSelector(on.any), all: normalizeSelector(on.all), none: normalizeSelector(on.none), flow, command, packages, image, skillsDir, instructions, resume, replicas, forge };
|
|
10534
|
+
return { type: "label", any: normalizeSelector(on.any), all: normalizeSelector(on.all), none: normalizeSelector(on.none), flow, command, packages, image, skillsDir, instructions, resume, secrets, secretsProfile, replicas, forge };
|
|
10416
10535
|
case "comment":
|
|
10417
|
-
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, command, packages, image, skillsDir, instructions, resume, replicas, forge };
|
|
10536
|
+
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, command, packages, image, skillsDir, instructions, resume, secrets, secretsProfile, replicas, forge };
|
|
10418
10537
|
case "pull_request":
|
|
10419
10538
|
return {
|
|
10420
10539
|
type: "pull_request",
|
|
@@ -10429,6 +10548,8 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
10429
10548
|
skillsDir,
|
|
10430
10549
|
instructions,
|
|
10431
10550
|
resume,
|
|
10551
|
+
secrets,
|
|
10552
|
+
secretsProfile,
|
|
10432
10553
|
replicas,
|
|
10433
10554
|
forge
|
|
10434
10555
|
};
|
|
@@ -38917,17 +39038,18 @@ function triggerLine(t) {
|
|
|
38917
39038
|
const ins = t?.instructions === true ? " [instructions]" : "";
|
|
38918
39039
|
const res = t?.resume === true ? " [resume]" : "";
|
|
38919
39040
|
const rep = t?.replicas > 1 ? ` [x${t.replicas}]` : "";
|
|
39041
|
+
const sec = t?.secrets > 0 ? ` [secrets ${t.secrets}${t.secretsProfile ? ` via ${t.secretsProfile}` : ""}]` : "";
|
|
38920
39042
|
switch (t?.type) {
|
|
38921
39043
|
case "cron":
|
|
38922
|
-
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"} \u2192 ${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
39044
|
+
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"} \u2192 ${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}`;
|
|
38923
39045
|
case "label":
|
|
38924
|
-
return `label ${ruleClauses(t) || "(no selector)"} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
39046
|
+
return `label ${ruleClauses(t) || "(no selector)"} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}`;
|
|
38925
39047
|
case "comment":
|
|
38926
|
-
return `comment "${t.phrase ?? "-"}" \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
39048
|
+
return `comment "${t.phrase ?? "-"}" \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}`;
|
|
38927
39049
|
case "pull_request": {
|
|
38928
39050
|
const clauses = ruleClauses(t);
|
|
38929
39051
|
const action = `action[${(t.action ?? []).join(",")}]`;
|
|
38930
|
-
return `pull_request ${action}${clauses ? ` ${clauses}` : ""} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
39052
|
+
return `pull_request ${action}${clauses ? ` ${clauses}` : ""} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}`;
|
|
38931
39053
|
}
|
|
38932
39054
|
default:
|
|
38933
39055
|
return "(unknown trigger)";
|
|
@@ -40038,6 +40160,12 @@ function renderTriggerDetail(t, inner, styler, sched = null, staged = null) {
|
|
|
40038
40160
|
keeps("issue text, file contents, tool output, the agent's own reasoning");
|
|
40039
40161
|
keeps("replayed into the next job on the same branch; forks never resume");
|
|
40040
40162
|
}
|
|
40163
|
+
if (t.secrets > 0) {
|
|
40164
|
+
const holds = (text) => out.push(fitLine(styler.fg("border", "\xB7 ") + styler.fg("warning", text), inner, styler));
|
|
40165
|
+
holds(`${t.secrets} vault secret(s) injected${t.secretsProfile ? ` via profile ${t.secretsProfile}` : ""}`);
|
|
40166
|
+
holds("resolved on the host before the container; the job holds no vault credential");
|
|
40167
|
+
holds("the agent can read them, and the forge is on the egress allowlist");
|
|
40168
|
+
}
|
|
40041
40169
|
if (t.replicas > 1) {
|
|
40042
40170
|
const races = (text) => out.push(fitLine(styler.fg("border", "\xB7 ") + styler.fg("warning", text), inner, styler));
|
|
40043
40171
|
races(`each delivery starts ${t.replicas} independent jobs`);
|
|
@@ -40867,8 +40995,8 @@ var init_setup_wizard = __esm({
|
|
|
40867
40995
|
init_deployment_pointer();
|
|
40868
40996
|
init_read_model();
|
|
40869
40997
|
init_index();
|
|
40870
|
-
RUNTIME_VERSION = "1.
|
|
40871
|
-
RECEIVER_VERSION = "1.
|
|
40998
|
+
RUNTIME_VERSION = "1.3.0";
|
|
40999
|
+
RECEIVER_VERSION = "1.2.0";
|
|
40872
41000
|
RUNTIME_PKG = "@edgehero/pi-dispatch";
|
|
40873
41001
|
RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
|
|
40874
41002
|
NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
|
|
@@ -40883,6 +41011,104 @@ var init_setup_wizard = __esm({
|
|
|
40883
41011
|
}
|
|
40884
41012
|
});
|
|
40885
41013
|
|
|
41014
|
+
// src/secrets-command.ts
|
|
41015
|
+
var secrets_command_exports = {};
|
|
41016
|
+
__export(secrets_command_exports, {
|
|
41017
|
+
runSecretsCommand: () => runSecretsCommand
|
|
41018
|
+
});
|
|
41019
|
+
function renderProfiles(profiles) {
|
|
41020
|
+
const names = Object.keys(profiles).sort();
|
|
41021
|
+
if (names.length === 0) return "No resolver profiles are declared in the settings overlay.";
|
|
41022
|
+
return ["Resolver profiles (settings overlay):", ...names.map((n) => ` ${n} -> ${profiles[n]}`)].join("\n");
|
|
41023
|
+
}
|
|
41024
|
+
async function runSecretsCommand(paths, ctx, notify, tokens, deps = {}) {
|
|
41025
|
+
const ui = ctx?.ui;
|
|
41026
|
+
const fsSeam = deps.fs ? { fs: deps.fs } : {};
|
|
41027
|
+
const view = readSettingsView({ settingsFile: paths.settingsFile, ...fsSeam });
|
|
41028
|
+
if (view.invalid) {
|
|
41029
|
+
notify?.(`settings overlay is unreadable (${view.invalid}) \u2014 fix it before declaring a profile`, "error");
|
|
41030
|
+
return;
|
|
41031
|
+
}
|
|
41032
|
+
const profiles = { ...view.overlay?.secretProfiles ?? {} };
|
|
41033
|
+
const action = tokens[0] ?? "list";
|
|
41034
|
+
if (action === "list") {
|
|
41035
|
+
notify?.(renderProfiles(profiles), "info");
|
|
41036
|
+
return;
|
|
41037
|
+
}
|
|
41038
|
+
if (typeof ui?.input !== "function" || typeof ui?.confirm !== "function") {
|
|
41039
|
+
notify?.("declaring a profile needs dialogs (newer pi) \u2014 no input/confirm available", "warning");
|
|
41040
|
+
return;
|
|
41041
|
+
}
|
|
41042
|
+
if (action === "add") {
|
|
41043
|
+
const nameAnswer = await ui.input("profile name \u2014 what a trigger's run.secretsProfile will say (e.g. prod)", "default");
|
|
41044
|
+
const name = (nameAnswer ?? "").trim();
|
|
41045
|
+
if (name === "") return;
|
|
41046
|
+
if (!PROFILE_NAME.test(name)) {
|
|
41047
|
+
notify?.("a profile name may use letters, digits, dot, dash and underscore only \u2014 PI_SECRET_PROFILES is a comma-separated list of name:path pairs", "error");
|
|
41048
|
+
return;
|
|
41049
|
+
}
|
|
41050
|
+
const pathAnswer = await ui.input("resolver \u2014 ABSOLUTE path to the script that reads one reference", "/opt/pi/resolve.sh");
|
|
41051
|
+
const path = (pathAnswer ?? "").trim();
|
|
41052
|
+
if (path === "") return;
|
|
41053
|
+
if (!(path.startsWith("/") || /^([A-Za-z]:[\\/]|\\\\)/.test(path))) {
|
|
41054
|
+
notify?.("the resolver needs an ABSOLUTE path: a service manager's working directory is not your shell's", "error");
|
|
41055
|
+
return;
|
|
41056
|
+
}
|
|
41057
|
+
const replacing = profiles[name] ? `
|
|
41058
|
+
|
|
41059
|
+
This REPLACES the current ${name} -> ${profiles[name]}` : "";
|
|
41060
|
+
const ok = await ui.confirm(
|
|
41061
|
+
"Declare a secret resolver",
|
|
41062
|
+
`${paths.settingsFile} gets:
|
|
41063
|
+
${JSON.stringify({ secretProfiles: { ...profiles, [name]: path } }, null, 2)}${replacing}
|
|
41064
|
+
|
|
41065
|
+
The worker EXECUTES this script, as the account it runs as, once per reference. It must also sit inside PI_SECRET_RESOLVER_ROOTS or the worker will refuse it.`
|
|
41066
|
+
);
|
|
41067
|
+
if (!ok) {
|
|
41068
|
+
notify?.("not declared", "info");
|
|
41069
|
+
return;
|
|
41070
|
+
}
|
|
41071
|
+
const res = writeSettings({ settingsFile: paths.settingsFile, mutate: (o) => ({ ...o, secretProfiles: { ...profiles, [name]: path } }), ...fsSeam });
|
|
41072
|
+
if (res.invalid) {
|
|
41073
|
+
notify?.(`rejected: ${res.invalid}`, "error");
|
|
41074
|
+
return;
|
|
41075
|
+
}
|
|
41076
|
+
notify?.(`declared ${name} -> ${path}. A trigger reaches it with "secretsProfile": "${name}" beside its "secrets" map, which is an edit to triggers.json.`, "info");
|
|
41077
|
+
return;
|
|
41078
|
+
}
|
|
41079
|
+
if (action === "remove") {
|
|
41080
|
+
const name = (tokens[1] ?? "").trim();
|
|
41081
|
+
if (!name || !(name in profiles)) {
|
|
41082
|
+
notify?.(`no such profile. ${renderProfiles(profiles)}`, "warning");
|
|
41083
|
+
return;
|
|
41084
|
+
}
|
|
41085
|
+
const ok = await ui.confirm("Remove a secret resolver", `${name} -> ${profiles[name]}
|
|
41086
|
+
|
|
41087
|
+
Every trigger naming it refuses PRE-SPEND after this, rather than running without its secrets.`);
|
|
41088
|
+
if (!ok) {
|
|
41089
|
+
notify?.("not removed", "info");
|
|
41090
|
+
return;
|
|
41091
|
+
}
|
|
41092
|
+
const next = { ...profiles };
|
|
41093
|
+
delete next[name];
|
|
41094
|
+
const res = writeSettings({ settingsFile: paths.settingsFile, mutate: (o) => ({ ...o, secretProfiles: next }), ...fsSeam });
|
|
41095
|
+
if (res.invalid) {
|
|
41096
|
+
notify?.(`rejected: ${res.invalid}`, "error");
|
|
41097
|
+
return;
|
|
41098
|
+
}
|
|
41099
|
+
notify?.(`removed ${name}`, "info");
|
|
41100
|
+
return;
|
|
41101
|
+
}
|
|
41102
|
+
notify?.("usage: /dispatch secrets <list|add|remove <name>>", "warning");
|
|
41103
|
+
}
|
|
41104
|
+
var PROFILE_NAME;
|
|
41105
|
+
var init_secrets_command = __esm({
|
|
41106
|
+
"src/secrets-command.ts"() {
|
|
41107
|
+
init_read_model();
|
|
41108
|
+
PROFILE_NAME = /^[A-Za-z0-9._-]+$/;
|
|
41109
|
+
}
|
|
41110
|
+
});
|
|
41111
|
+
|
|
40886
41112
|
// src/index.ts
|
|
40887
41113
|
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
40888
41114
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
@@ -41458,6 +41684,11 @@ ${renderBudget({ budget, settings: settings2 })}`);
|
|
|
41458
41684
|
applyUnset(paths.settingsFile, tokens, notify);
|
|
41459
41685
|
return;
|
|
41460
41686
|
}
|
|
41687
|
+
case "secrets": {
|
|
41688
|
+
const { runSecretsCommand: runSecretsCommand2 } = await Promise.resolve().then(() => (init_secrets_command(), secrets_command_exports));
|
|
41689
|
+
await runSecretsCommand2(paths, ctx, notify, tokens.slice(1));
|
|
41690
|
+
return;
|
|
41691
|
+
}
|
|
41461
41692
|
case "setup": {
|
|
41462
41693
|
const { runSetupWizard: runSetupWizard2 } = await Promise.resolve().then(() => (init_setup_wizard(), setup_wizard_exports));
|
|
41463
41694
|
await runSetupWizard2(paths, ctx, notify, { openDashboardFn: openDashboard });
|
|
@@ -42063,7 +42294,7 @@ var init_index = __esm({
|
|
|
42063
42294
|
runtimeSkewNoticed = false;
|
|
42064
42295
|
CHANNEL = "pi-dispatch-admin";
|
|
42065
42296
|
REBUILT_NOTICE = (reason) => `replaced invalid settings file (${reason}) \u2014 other keys were lost`;
|
|
42066
|
-
USAGE = "usage: /dispatch <status|pause|resume|run|runs|logs|budget|insights|triggers|settings|set|unset|setup>";
|
|
42297
|
+
USAGE = "usage: /dispatch <status|pause|resume|run|runs|logs|budget|insights|triggers|settings|set|unset|setup|secrets>";
|
|
42067
42298
|
KNOWN_SUBCOMMANDS = [
|
|
42068
42299
|
"status",
|
|
42069
42300
|
"pause",
|
|
@@ -42077,7 +42308,8 @@ var init_index = __esm({
|
|
|
42077
42308
|
"settings",
|
|
42078
42309
|
"set",
|
|
42079
42310
|
"unset",
|
|
42080
|
-
"setup"
|
|
42311
|
+
"setup",
|
|
42312
|
+
"secrets"
|
|
42081
42313
|
];
|
|
42082
42314
|
FORGE_PROMPT = "forge \u2014 github, gitlab, forgejo or azure";
|
|
42083
42315
|
PR_ACTION_VOCAB = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgehero/pi-dispatch-admin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Operator console (a pi extension) + skill for pi-dispatch: run the pi coding agent as a self-hosted service. A /dispatch TUI for the queue, spend caps, run history, editable GitHub, GitLab, Forgejo and Azure DevOps triggers (cron/label/comment/PR/MR/work item), and scheduled pause windows — plus AI-operable, human-confirmed controls. Runs against a live pi-dispatch deployment.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -120,6 +120,36 @@ cannot, and that it is an edit they make to `triggers.json` themselves.** Do not
|
|
|
120
120
|
built or pulled on the worker's own host, because jobs run with `--pull=never` and nothing is fetched at
|
|
121
121
|
job time; and `pi-dispatch doctor` lists every image their triggers name and flags one that is missing.
|
|
122
122
|
|
|
123
|
+
## Vault secrets — `run.secrets`, and why you cannot set it
|
|
124
|
+
|
|
125
|
+
A trigger may carry `run.secrets`: a map of environment variable name to an opaque **reference**, plus
|
|
126
|
+
`run.secretsProfile` naming which of the operator's resolvers reads them. Before the container starts, the
|
|
127
|
+
worker runs that resolver once per reference on the HOST and injects the values into the job's environment.
|
|
128
|
+
It exists so one trigger can hold a deploy key while every other job on the deployment holds none.
|
|
129
|
+
|
|
130
|
+
Report it when asked, and be precise about the shape of the trade. **The job holds no vault credential.** It
|
|
131
|
+
receives values, never the thing that can fetch values, so it cannot enumerate the vault or reach anything
|
|
132
|
+
the operator did not name in a reviewed file. What that does *not* do is bound where a value can go once the
|
|
133
|
+
agent has it: the agent can read its own environment, and the forge is on the egress allowlist by necessity.
|
|
134
|
+
The panel shows the count and the profile name in the trigger list and in the drill-in, never the references.
|
|
135
|
+
|
|
136
|
+
**You cannot set it, in either direction.** No `dispatch_*` tool has a `secrets` parameter, and none has a
|
|
137
|
+
`secretsProfile` parameter either. The second absence is worth understanding rather than treating as an
|
|
138
|
+
oversight: a profile that resolves nothing is refused at load, and no tool can write `run.secrets`, so a
|
|
139
|
+
profile picker could never produce a valid trigger even once. It would be a control that looks like a grant
|
|
140
|
+
and is only ever an error.
|
|
141
|
+
|
|
142
|
+
You also cannot declare a **resolver profile**. That is `/dispatch secrets add`, which the operator types
|
|
143
|
+
themselves, because declaring one means naming an absolute host path the worker executes.
|
|
144
|
+
|
|
145
|
+
So if a user asks you to give a trigger access to a vault, or to wire up 1Password: **say plainly that you
|
|
146
|
+
cannot, and that both halves are theirs.** Three useful things you *can* say: declaring the manager is
|
|
147
|
+
`/dispatch secrets add`, and it is two questions (a name and the path to a one-line script such as
|
|
148
|
+
`exec op read --no-newline "$1"`); binding it to a trigger is an edit to `triggers.json` beside that
|
|
149
|
+
trigger's `flow`; and `pi-dispatch doctor` lists every declared profile, fails loudly when a trigger names
|
|
150
|
+
one that is not declared, and warns when a **local** trigger binds secrets, because a local job edits the
|
|
151
|
+
operator's own folder in place and a credential the agent writes into `.env` lands in their real repository.
|
|
152
|
+
|
|
123
153
|
## The forge a trigger listens to — `run.kind`
|
|
124
154
|
|
|
125
155
|
A webhook trigger names its forge: `"kind"` is `github`, `gitlab`, `forgejo` or `azure`. Everything else
|