switchroom 0.13.35 → 0.13.36
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/switchroom.js +92 -7
- package/dist/host-control/main.js +80 -32
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +158 -26
- package/telegram-plugin/gateway/config-approval-handler.test.ts +188 -1
- package/telegram-plugin/gateway/config-approval-handler.ts +170 -15
- package/telegram-plugin/gateway/diff-preview-card.test.ts +2 -2
- package/telegram-plugin/gateway/diff-preview-card.ts +2 -2
- package/telegram-plugin/gateway/drive-write-approval.test.ts +70 -0
- package/telegram-plugin/gateway/drive-write-approval.ts +51 -2
- package/telegram-plugin/gateway/gateway.ts +42 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +10 -1
- package/telegram-plugin/gateway/oversize-card-body.test.ts +108 -0
- package/telegram-plugin/gateway/oversize-card-body.ts +114 -0
- package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +118 -41
- package/telegram-plugin/hooks/silent-end-scan.mjs +190 -0
- package/telegram-plugin/pending-work-progress.ts +37 -1
- package/telegram-plugin/tests/pending-work-progress.test.ts +134 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +242 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +314 -0
- package/telegram-plugin/tests/silent-end.test.ts +122 -38
package/dist/cli/switchroom.js
CHANGED
|
@@ -47711,8 +47711,8 @@ var {
|
|
|
47711
47711
|
} = import__.default;
|
|
47712
47712
|
|
|
47713
47713
|
// src/build-info.ts
|
|
47714
|
-
var VERSION = "0.13.
|
|
47715
|
-
var COMMIT_SHA = "
|
|
47714
|
+
var VERSION = "0.13.36";
|
|
47715
|
+
var COMMIT_SHA = "73e8bb05";
|
|
47716
47716
|
|
|
47717
47717
|
// src/cli/agent.ts
|
|
47718
47718
|
init_source();
|
|
@@ -61580,6 +61580,23 @@ function registerVaultBackupCommand(vault, program3) {
|
|
|
61580
61580
|
});
|
|
61581
61581
|
}
|
|
61582
61582
|
|
|
61583
|
+
// src/cli/vault-denied-envelope.ts
|
|
61584
|
+
var ENVELOPE_SENTINEL = "ERROR-ENVELOPE:";
|
|
61585
|
+
function writeVaultDeniedEnvelope(vaultKey, brokerCode, human) {
|
|
61586
|
+
const envelope = {
|
|
61587
|
+
v: 1,
|
|
61588
|
+
code: "VAULT-BROKER-DENIED",
|
|
61589
|
+
human: `${brokerCode}: ${human}`,
|
|
61590
|
+
fix: {
|
|
61591
|
+
kind: "request_vault_grant",
|
|
61592
|
+
vault_key: vaultKey
|
|
61593
|
+
},
|
|
61594
|
+
request_id: `vault-cli-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
|
61595
|
+
};
|
|
61596
|
+
process.stderr.write(`${ENVELOPE_SENTINEL} ${JSON.stringify(envelope)}
|
|
61597
|
+
`);
|
|
61598
|
+
}
|
|
61599
|
+
|
|
61583
61600
|
// src/cli/vault.ts
|
|
61584
61601
|
function isSandboxContext() {
|
|
61585
61602
|
return process.env.SWITCHROOM_RUNTIME === "docker";
|
|
@@ -61805,6 +61822,7 @@ function registerVaultCommand(program3) {
|
|
|
61805
61822
|
}
|
|
61806
61823
|
process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
|
|
61807
61824
|
`);
|
|
61825
|
+
writeVaultDeniedEnvelope(key, result.code, result.msg);
|
|
61808
61826
|
process.exit(VAULT_EXIT_DENIED);
|
|
61809
61827
|
}
|
|
61810
61828
|
if (inSandbox) {
|
|
@@ -62020,6 +62038,7 @@ Push passphrase to broker for future requests? [Y/n]: `);
|
|
|
62020
62038
|
process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
|
|
62021
62039
|
` + `${recoveryHint("denied", key)}
|
|
62022
62040
|
`);
|
|
62041
|
+
writeVaultDeniedEnvelope(key, result.code, result.msg);
|
|
62023
62042
|
process.exit(2);
|
|
62024
62043
|
}
|
|
62025
62044
|
} else {
|
|
@@ -76198,7 +76217,34 @@ function denyPendingScheduleEntry(opts) {
|
|
|
76198
76217
|
|
|
76199
76218
|
// src/cli/agent-config-write.ts
|
|
76200
76219
|
import { existsSync as existsSync73, readFileSync as readFileSync59 } from "node:fs";
|
|
76220
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
76201
76221
|
var MAX_ENTRIES_PER_AGENT = 20;
|
|
76222
|
+
var MIN_CRON_INTERVAL_MIN = 5;
|
|
76223
|
+
function extractCronSmallestGapMin(expr) {
|
|
76224
|
+
const fields = expr.trim().split(/\s+/);
|
|
76225
|
+
if (fields.length < 5)
|
|
76226
|
+
return 0;
|
|
76227
|
+
const min = fields[0];
|
|
76228
|
+
if (min === "*")
|
|
76229
|
+
return 1;
|
|
76230
|
+
const step = min.match(/^\*\/(\d+)$/);
|
|
76231
|
+
if (step)
|
|
76232
|
+
return Number(step[1]);
|
|
76233
|
+
if (min.includes(",")) {
|
|
76234
|
+
const parts = min.split(",").map((s) => Number(s)).filter((n) => Number.isFinite(n));
|
|
76235
|
+
if (parts.length >= 2) {
|
|
76236
|
+
const sorted = [...parts].sort((a, b) => a - b);
|
|
76237
|
+
let smallest = Infinity;
|
|
76238
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
76239
|
+
const gap = sorted[i] - sorted[i - 1];
|
|
76240
|
+
if (gap > 0 && gap < smallest)
|
|
76241
|
+
smallest = gap;
|
|
76242
|
+
}
|
|
76243
|
+
return Number.isFinite(smallest) ? smallest : 0;
|
|
76244
|
+
}
|
|
76245
|
+
}
|
|
76246
|
+
return 0;
|
|
76247
|
+
}
|
|
76202
76248
|
function checkOperatorContext(verb, env2 = process.env) {
|
|
76203
76249
|
if (env2.SWITCHROOM_OPERATOR === "1")
|
|
76204
76250
|
return { ok: true };
|
|
@@ -76211,8 +76257,45 @@ function checkOperatorContext(verb, env2 = process.env) {
|
|
|
76211
76257
|
}
|
|
76212
76258
|
return { ok: true };
|
|
76213
76259
|
}
|
|
76260
|
+
function buildEnvelopeForCode(code, message, extra) {
|
|
76261
|
+
const request_id = `agent-config-${randomUUID5()}`;
|
|
76262
|
+
if (code === "E_CRON_TOO_FREQUENT") {
|
|
76263
|
+
return {
|
|
76264
|
+
v: 1,
|
|
76265
|
+
code,
|
|
76266
|
+
human: message,
|
|
76267
|
+
fix: {
|
|
76268
|
+
kind: "quota_exceeded",
|
|
76269
|
+
quota: "cron_min_interval_minutes",
|
|
76270
|
+
current: typeof extra.requested_interval_min === "number" ? extra.requested_interval_min : 0,
|
|
76271
|
+
limit: MIN_CRON_INTERVAL_MIN
|
|
76272
|
+
},
|
|
76273
|
+
request_id
|
|
76274
|
+
};
|
|
76275
|
+
}
|
|
76276
|
+
if (code === "E_QUOTA_EXCEEDED") {
|
|
76277
|
+
const current = typeof extra.current === "number" ? extra.current : MAX_ENTRIES_PER_AGENT;
|
|
76278
|
+
return {
|
|
76279
|
+
v: 1,
|
|
76280
|
+
code,
|
|
76281
|
+
human: message,
|
|
76282
|
+
fix: {
|
|
76283
|
+
kind: "quota_exceeded",
|
|
76284
|
+
quota: "schedule_entries_per_agent",
|
|
76285
|
+
current,
|
|
76286
|
+
limit: MAX_ENTRIES_PER_AGENT
|
|
76287
|
+
},
|
|
76288
|
+
request_id
|
|
76289
|
+
};
|
|
76290
|
+
}
|
|
76291
|
+
return;
|
|
76292
|
+
}
|
|
76214
76293
|
function emitError(code, message, extra = {}) {
|
|
76215
|
-
|
|
76294
|
+
const error_envelope = buildEnvelopeForCode(code, message, extra);
|
|
76295
|
+
const line = { code, message, ...extra };
|
|
76296
|
+
if (error_envelope)
|
|
76297
|
+
line.error_envelope = error_envelope;
|
|
76298
|
+
process.stderr.write(JSON.stringify(line) + `
|
|
76216
76299
|
`);
|
|
76217
76300
|
}
|
|
76218
76301
|
function exitCodeFor(code) {
|
|
@@ -76276,7 +76359,8 @@ function scheduleAdd(opts) {
|
|
|
76276
76359
|
ok: false,
|
|
76277
76360
|
code: "E_CRON_TOO_FREQUENT",
|
|
76278
76361
|
message: "cron interval is tighter than the minimum (5 minutes)",
|
|
76279
|
-
exit: 9
|
|
76362
|
+
exit: 9,
|
|
76363
|
+
meta: { requested_interval_min: extractCronSmallestGapMin(opts.cronExpr) }
|
|
76280
76364
|
};
|
|
76281
76365
|
}
|
|
76282
76366
|
const rej = filterOverlaySecrets(dry.doc, "overlay");
|
|
@@ -76294,7 +76378,8 @@ function scheduleAdd(opts) {
|
|
|
76294
76378
|
ok: false,
|
|
76295
76379
|
code: "E_QUOTA_EXCEEDED",
|
|
76296
76380
|
message: `agent already has ${existing.length} overlay entries (max ${MAX_ENTRIES_PER_AGENT})`,
|
|
76297
|
-
exit: 9
|
|
76381
|
+
exit: 9,
|
|
76382
|
+
meta: { current: existing.length }
|
|
76298
76383
|
};
|
|
76299
76384
|
}
|
|
76300
76385
|
const hash2 = cronUnitHash(opts.cronExpr, opts.prompt);
|
|
@@ -76493,7 +76578,7 @@ function registerAgentConfigWriteCommands(program3) {
|
|
|
76493
76578
|
else if (process.env.SWITCHROOM_AGENT_NAME)
|
|
76494
76579
|
resolvedAgent = process.env.SWITCHROOM_AGENT_NAME;
|
|
76495
76580
|
if (!r.ok) {
|
|
76496
|
-
emitError(r.code, r.message);
|
|
76581
|
+
emitError(r.code, r.message, r.meta ?? {});
|
|
76497
76582
|
appendAudit(resolvedAgent, "schedule.add", { cron: opts.cron, prompt: opts.prompt, name: opts.name, code: r.code, would_recreate: false }, r.exit);
|
|
76498
76583
|
process.exit(r.exit);
|
|
76499
76584
|
}
|
|
@@ -76606,7 +76691,7 @@ function registerAgentConfigWriteCommands(program3) {
|
|
|
76606
76691
|
else if (process.env.SWITCHROOM_AGENT_NAME)
|
|
76607
76692
|
resolvedAgent = process.env.SWITCHROOM_AGENT_NAME;
|
|
76608
76693
|
if (!r.ok) {
|
|
76609
|
-
emitError(r.code, r.message);
|
|
76694
|
+
emitError(r.code, r.message, r.meta ?? {});
|
|
76610
76695
|
appendAudit(resolvedAgent, "schedule.remove", { ...opts, code: r.code }, r.exit);
|
|
76611
76696
|
process.exit(r.exit);
|
|
76612
76697
|
}
|
|
@@ -14920,16 +14920,6 @@ function deniedResponse(request_id, error, duration_ms = 0) {
|
|
|
14920
14920
|
error
|
|
14921
14921
|
};
|
|
14922
14922
|
}
|
|
14923
|
-
function errorResponse(request_id, error, duration_ms = 0) {
|
|
14924
|
-
return {
|
|
14925
|
-
v: 1,
|
|
14926
|
-
request_id,
|
|
14927
|
-
result: "error",
|
|
14928
|
-
exit_code: null,
|
|
14929
|
-
duration_ms,
|
|
14930
|
-
error
|
|
14931
|
-
};
|
|
14932
|
-
}
|
|
14933
14923
|
|
|
14934
14924
|
// src/analytics/error-friction.ts
|
|
14935
14925
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -19515,6 +19505,11 @@ class ErrorBuilder {
|
|
|
19515
19505
|
return this;
|
|
19516
19506
|
}
|
|
19517
19507
|
docs(url) {
|
|
19508
|
+
try {
|
|
19509
|
+
new URL(url);
|
|
19510
|
+
} catch {
|
|
19511
|
+
throw new TypeError(`err().docs(): invalid URL: ${JSON.stringify(url)} — ErrorEnvelopeSchema requires a fully-qualified URL`);
|
|
19512
|
+
}
|
|
19518
19513
|
this._docs = url;
|
|
19519
19514
|
return this;
|
|
19520
19515
|
}
|
|
@@ -20391,6 +20386,14 @@ var CONFIG_APPROVAL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
20391
20386
|
function defaultApprovalId() {
|
|
20392
20387
|
return randomBytes(4).toString("hex");
|
|
20393
20388
|
}
|
|
20389
|
+
function formatConfigApprovalDenyError(approval, approvalId) {
|
|
20390
|
+
if (approval.denySource === "dispatch_failure") {
|
|
20391
|
+
const detail = approval.reason ?? "card dispatch failed";
|
|
20392
|
+
return `E_APPROVAL_DISPATCH_FAILED: ${detail} (approval_id=${approvalId})`;
|
|
20393
|
+
}
|
|
20394
|
+
const suffix = approval.reason ? `: ${approval.reason}` : "";
|
|
20395
|
+
return `E_DENIED: operator denied config_propose_edit${suffix} (approval_id=${approvalId})`;
|
|
20396
|
+
}
|
|
20394
20397
|
function unlinkSyncBestEffort(path2) {
|
|
20395
20398
|
try {
|
|
20396
20399
|
unlinkSync(path2);
|
|
@@ -20627,8 +20630,10 @@ class HostdServer {
|
|
|
20627
20630
|
resp = await this.handleConfigProposeEdit(req, caller, started);
|
|
20628
20631
|
break;
|
|
20629
20632
|
}
|
|
20630
|
-
} catch (
|
|
20631
|
-
|
|
20633
|
+
} catch (e) {
|
|
20634
|
+
const msg = e.message;
|
|
20635
|
+
resp = err("E_DISPATCH_FAILED", "hostd dispatch failed").why(msg).op(req.op).caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
|
|
20636
|
+
resp = { ...resp, error: `hostd dispatch failed: ${msg}` };
|
|
20632
20637
|
}
|
|
20633
20638
|
await this.writeAudit({ caller, req, resp });
|
|
20634
20639
|
socket.write(encodeResponse(resp));
|
|
@@ -20924,7 +20929,7 @@ class HostdServer {
|
|
|
20924
20929
|
async handleConfigProposeEdit(req, caller, started) {
|
|
20925
20930
|
const enabled = this.opts.config.hostd?.config_edit_enabled === true;
|
|
20926
20931
|
if (!enabled) {
|
|
20927
|
-
return err("E_CONFIG_EDIT_DISABLED", "config_propose_edit is disabled").why("operator opt-in per RFC §3.3").fixFlipFlag("hostd.config_edit_enabled", true).docs("https://switchroom.dev/docs/config-edit#opt-in").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
|
|
20932
|
+
return err("E_CONFIG_EDIT_DISABLED", "config_propose_edit is disabled").why("operator opt-in per RFC §3.3").fixFlipFlag("hostd.config_edit_enabled", true).docs("https://switchroom.dev/docs/config-edit#opt-in").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
|
|
20928
20933
|
}
|
|
20929
20934
|
const configPath = this.opts.configPath ?? req.args.target_path;
|
|
20930
20935
|
const verdict = validateConfigEdit({
|
|
@@ -20933,10 +20938,12 @@ class HostdServer {
|
|
|
20933
20938
|
unifiedDiff: req.args.unified_diff
|
|
20934
20939
|
});
|
|
20935
20940
|
if (!verdict.ok) {
|
|
20936
|
-
return
|
|
20941
|
+
return err(verdict.code, verdict.detail).fixBadInput("unified_diff").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
|
|
20937
20942
|
}
|
|
20938
20943
|
if (!this.opts.approvalGateway) {
|
|
20939
|
-
return
|
|
20944
|
+
return err("E_NO_APPROVAL_GATEWAY", "validation passed but hostd was started without an approval-gateway wiring; the operator build is missing the telegram-plugin link").fixOperatorAction("infra", [
|
|
20945
|
+
"ensure hostd was launched with --approval-gateway / telegram-plugin link"
|
|
20946
|
+
]).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
|
|
20940
20947
|
}
|
|
20941
20948
|
const callerName = caller.kind === "agent" ? caller.name : "operator";
|
|
20942
20949
|
const approvalId = (this.opts.generateApprovalId ?? defaultApprovalId)();
|
|
@@ -20948,35 +20955,45 @@ class HostdServer {
|
|
|
20948
20955
|
timeoutMs: CONFIG_APPROVAL_TIMEOUT_MS
|
|
20949
20956
|
});
|
|
20950
20957
|
if (approval.verdict === "deny") {
|
|
20951
|
-
|
|
20958
|
+
const legacy = formatConfigApprovalDenyError(approval, approvalId);
|
|
20959
|
+
const isDispatchFailure = approval.denySource === "dispatch_failure";
|
|
20960
|
+
const code = isDispatchFailure ? "E_APPROVAL_DISPATCH_FAILED" : "E_DENIED";
|
|
20961
|
+
const human = isDispatchFailure ? "approval card dispatch failed before the operator could see it" : "operator denied config_propose_edit";
|
|
20962
|
+
const b = err(code, human).why(legacy).fixOperatorAction(isDispatchFailure ? "infra" : "policy_denied", isDispatchFailure ? ["check telegram-plugin gateway connectivity"] : undefined).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined);
|
|
20963
|
+
if (!isDispatchFailure)
|
|
20964
|
+
b.asDenied();
|
|
20965
|
+
const built = b.build(req.request_id, Date.now() - started);
|
|
20966
|
+
return { ...built, error: legacy };
|
|
20952
20967
|
}
|
|
20953
20968
|
if (approval.verdict === "timeout") {
|
|
20954
|
-
|
|
20969
|
+
const legacy = `E_APPROVAL_TIMEOUT: operator approval card expired without a tap (approval_id=${approvalId})`;
|
|
20970
|
+
const built = err("E_APPROVAL_TIMEOUT", "operator approval card expired without a tap").why(`approval_id=${approvalId}`).fixOperatorAction("policy_denied").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
|
|
20971
|
+
return { ...built, error: legacy };
|
|
20955
20972
|
}
|
|
20956
20973
|
const release = await this.acquireConfigApplyLock();
|
|
20957
20974
|
try {
|
|
20958
20975
|
let snapshot;
|
|
20959
20976
|
try {
|
|
20960
20977
|
snapshot = readFileSync5(configPath, "utf-8");
|
|
20961
|
-
} catch (
|
|
20978
|
+
} catch (e) {
|
|
20962
20979
|
await approval.finalize({
|
|
20963
20980
|
outcome: "reconcile_failed_rolled_back",
|
|
20964
|
-
detail: `pre-write snapshot read failed: ${
|
|
20981
|
+
detail: `pre-write snapshot read failed: ${e.message}`
|
|
20965
20982
|
});
|
|
20966
|
-
return
|
|
20983
|
+
return this.reconcileFailedRolledBack(`snapshot read failed: ${e.message}`, req, caller, started);
|
|
20967
20984
|
}
|
|
20968
20985
|
const postApply = verdict.postApplyContent;
|
|
20969
20986
|
const tmp = configPath + ".tmp";
|
|
20970
20987
|
try {
|
|
20971
20988
|
writeFileSync3(tmp, postApply);
|
|
20972
20989
|
renameSync(tmp, configPath);
|
|
20973
|
-
} catch (
|
|
20990
|
+
} catch (e) {
|
|
20974
20991
|
unlinkSyncBestEffort(tmp);
|
|
20975
20992
|
await approval.finalize({
|
|
20976
20993
|
outcome: "reconcile_failed_rolled_back",
|
|
20977
|
-
detail: `atomic write failed: ${
|
|
20994
|
+
detail: `atomic write failed: ${e.message}`
|
|
20978
20995
|
});
|
|
20979
|
-
return
|
|
20996
|
+
return this.reconcileFailedRolledBack(`write failed: ${e.message}`, req, caller, started);
|
|
20980
20997
|
}
|
|
20981
20998
|
const runner = this.opts.runReconcile ?? (async () => this.runSwitchroom(["apply"]));
|
|
20982
20999
|
const recRes = await runner({ requestId: approvalId });
|
|
@@ -20996,13 +21013,13 @@ class HostdServer {
|
|
|
20996
21013
|
try {
|
|
20997
21014
|
writeFileSync3(tmp, snapshot);
|
|
20998
21015
|
renameSync(tmp, configPath);
|
|
20999
|
-
} catch (
|
|
21000
|
-
rollbackDetail = `snapshot restore failed: ${
|
|
21016
|
+
} catch (e) {
|
|
21017
|
+
rollbackDetail = `snapshot restore failed: ${e.message}`;
|
|
21001
21018
|
await approval.finalize({
|
|
21002
21019
|
outcome: "reconcile_failed_rolled_back",
|
|
21003
21020
|
detail: rollbackDetail
|
|
21004
21021
|
});
|
|
21005
|
-
return
|
|
21022
|
+
return this.reconcileFailedRolledBack(rollbackDetail, req, caller, started);
|
|
21006
21023
|
}
|
|
21007
21024
|
const recRes2 = await runner({ requestId: approvalId });
|
|
21008
21025
|
const recoveryNote = recRes2.exit_code === 0 ? "rolled back successfully" : `rolled back but recovery reconcile also failed (exit ${recRes2.exit_code})`;
|
|
@@ -21010,11 +21027,18 @@ class HostdServer {
|
|
|
21010
21027
|
outcome: "reconcile_failed_rolled_back",
|
|
21011
21028
|
detail: recoveryNote
|
|
21012
21029
|
});
|
|
21013
|
-
return
|
|
21030
|
+
return this.reconcileFailedRolledBack(`reconcile exit ${recRes.exit_code}; ${recoveryNote}`, req, caller, started);
|
|
21014
21031
|
} finally {
|
|
21015
21032
|
release();
|
|
21016
21033
|
}
|
|
21017
21034
|
}
|
|
21035
|
+
reconcileFailedRolledBack(detail, req, caller, started) {
|
|
21036
|
+
const legacy = `E_RECONCILE_FAILED_ROLLED_BACK: ${detail}`;
|
|
21037
|
+
const built = err("E_RECONCILE_FAILED_ROLLED_BACK", "config write or reconcile failed; live file rolled back to snapshot").why(detail).fixOperatorAction("infra", [
|
|
21038
|
+
"inspect hostd logs + switchroom apply output to identify the underlying failure"
|
|
21039
|
+
]).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
|
|
21040
|
+
return { ...built, error: legacy };
|
|
21041
|
+
}
|
|
21018
21042
|
configApplyLock = Promise.resolve();
|
|
21019
21043
|
async acquireConfigApplyLock() {
|
|
21020
21044
|
let release;
|
|
@@ -21180,7 +21204,9 @@ class HostdServer {
|
|
|
21180
21204
|
handleGetStatus(req, _caller, started) {
|
|
21181
21205
|
const entry = this.statusByRequestId.get(req.args.target_request_id);
|
|
21182
21206
|
if (!entry) {
|
|
21183
|
-
|
|
21207
|
+
const legacy = `get_status: internal: entry missing despite gate accept`;
|
|
21208
|
+
const built = err("E_INTERNAL", "entry missing despite gate accept").op("get_status").build(req.request_id, Date.now() - started);
|
|
21209
|
+
return { ...built, error: legacy };
|
|
21184
21210
|
}
|
|
21185
21211
|
return this.statusEntryToResponse(req.request_id, entry);
|
|
21186
21212
|
}
|
|
@@ -21339,6 +21365,8 @@ class SocketApprovalGateway {
|
|
|
21339
21365
|
if (sockPath === null) {
|
|
21340
21366
|
return {
|
|
21341
21367
|
verdict: "deny",
|
|
21368
|
+
reason: "no reachable telegram gateway socket for this agent",
|
|
21369
|
+
denySource: "dispatch_failure",
|
|
21342
21370
|
finalize: async () => {}
|
|
21343
21371
|
};
|
|
21344
21372
|
}
|
|
@@ -21379,7 +21407,12 @@ class SocketApprovalGateway {
|
|
|
21379
21407
|
return;
|
|
21380
21408
|
resolved = true;
|
|
21381
21409
|
log(`request_config_approval write failed (requestId=${req.requestId}): ${err2.message}`);
|
|
21382
|
-
resolve6({
|
|
21410
|
+
resolve6({
|
|
21411
|
+
verdict: "deny",
|
|
21412
|
+
reason: `request_config_approval write failed: ${err2.message}`,
|
|
21413
|
+
denySource: "dispatch_failure",
|
|
21414
|
+
finalize: async () => {}
|
|
21415
|
+
});
|
|
21383
21416
|
}
|
|
21384
21417
|
});
|
|
21385
21418
|
client2.on("data", (chunk2) => {
|
|
@@ -21400,8 +21433,13 @@ class SocketApprovalGateway {
|
|
|
21400
21433
|
const obj = parsed;
|
|
21401
21434
|
if (obj.type === "config_approval_resolved" && obj.requestId === req.requestId && (obj.verdict === "approve" || obj.verdict === "deny" || obj.verdict === "timeout") && !resolved) {
|
|
21402
21435
|
resolved = true;
|
|
21436
|
+
const verdict = obj.verdict;
|
|
21437
|
+
const reasonField = typeof obj.reason === "string" ? obj.reason : undefined;
|
|
21438
|
+
const denySource = verdict === "deny" ? obj.denySource === "dispatch_failure" ? "dispatch_failure" : "operator" : undefined;
|
|
21403
21439
|
resolve6({
|
|
21404
|
-
verdict
|
|
21440
|
+
verdict,
|
|
21441
|
+
...reasonField !== undefined ? { reason: reasonField } : {},
|
|
21442
|
+
...denySource !== undefined ? { denySource } : {},
|
|
21405
21443
|
finalize
|
|
21406
21444
|
});
|
|
21407
21445
|
}
|
|
@@ -21412,13 +21450,23 @@ class SocketApprovalGateway {
|
|
|
21412
21450
|
return;
|
|
21413
21451
|
resolved = true;
|
|
21414
21452
|
log(`gateway socket error (requestId=${req.requestId}): ${err2.message}`);
|
|
21415
|
-
resolve6({
|
|
21453
|
+
resolve6({
|
|
21454
|
+
verdict: "deny",
|
|
21455
|
+
reason: `gateway socket error: ${err2.message}`,
|
|
21456
|
+
denySource: "dispatch_failure",
|
|
21457
|
+
finalize: async () => {}
|
|
21458
|
+
});
|
|
21416
21459
|
});
|
|
21417
21460
|
client2.on("close", () => {
|
|
21418
21461
|
if (resolved)
|
|
21419
21462
|
return;
|
|
21420
21463
|
resolved = true;
|
|
21421
|
-
resolve6({
|
|
21464
|
+
resolve6({
|
|
21465
|
+
verdict: "deny",
|
|
21466
|
+
reason: "gateway socket closed before verdict",
|
|
21467
|
+
denySource: "dispatch_failure",
|
|
21468
|
+
finalize: async () => {}
|
|
21469
|
+
});
|
|
21422
21470
|
});
|
|
21423
21471
|
});
|
|
21424
21472
|
}
|
package/package.json
CHANGED