switchroom 0.19.18 → 0.19.19
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/agent-scheduler/index.js +2 -1
- package/dist/auth-broker/index.js +3 -1
- package/dist/cli/drive-write-pretool.mjs +48 -5
- package/dist/cli/ms-365-write-pretool.mjs +40 -2
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +3392 -1569
- package/dist/host-control/main.js +12209 -11396
- package/dist/vault/approvals/kernel-server.js +60 -7
- package/dist/vault/broker/server.js +206 -76
- package/package.json +4 -3
- package/profiles/_base/start.sh.hbs +61 -1
- package/telegram-plugin/bridge/bridge.ts +14 -0
- package/telegram-plugin/dist/bridge/bridge.js +13 -0
- package/telegram-plugin/dist/gateway/gateway.js +1644 -1044
- package/telegram-plugin/dist/server.js +13 -0
- package/telegram-plugin/gateway/always-allow-persist-queue.ts +97 -11
- package/telegram-plugin/gateway/missed-approvals-store.ts +66 -17
- package/telegram-plugin/gateway/pending-card-store.ts +46 -16
- package/telegram-plugin/gateway/scoped-grant-store.ts +39 -14
- package/telegram-plugin/gateway/store-file.ts +244 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +88 -2
- package/telegram-plugin/tests/bridge-tool-parity.test.ts +95 -0
- package/telegram-plugin/tests/store-atomic-write.test.ts +411 -0
- package/telegram-plugin/tests/tool-activity-summary.test.ts +9 -2
- package/telegram-plugin/tests/tool-label-pretool.test.ts +94 -0
- package/telegram-plugin/tests/worker-feed-repeat-steps.test.ts +147 -0
- package/telegram-plugin/worker-activity-feed.ts +51 -1
- package/vendor/hindsight-memory/scripts/drain_pending.py +668 -56
- package/vendor/hindsight-memory/scripts/lib/client.py +124 -0
- package/vendor/hindsight-memory/scripts/lib/pending.py +865 -33
- package/vendor/hindsight-memory/scripts/lib/retain_split.py +449 -0
- package/vendor/hindsight-memory/scripts/session_start.py +48 -0
- package/vendor/hindsight-memory/scripts/tests/test_client_document_exists.py +470 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +2121 -0
- package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +430 -0
- package/vendor/hindsight-memory/scripts/tests/test_session_start_version_skew.py +204 -0
- package/vendor/hindsight-memory/tests/test_drain_pending.py +102 -6
- package/vendor/hindsight-memory/tests/test_pending.py +32 -7
|
@@ -18546,7 +18546,8 @@ var ApprovalDecisionMetaSchema = exports_external.object({
|
|
|
18546
18546
|
ttl_expires_at: exports_external.number().nullable(),
|
|
18547
18547
|
last_used_at: exports_external.number().nullable(),
|
|
18548
18548
|
revoked_at: exports_external.number().nullable(),
|
|
18549
|
-
revoke_reason: exports_external.string().nullable()
|
|
18549
|
+
revoke_reason: exports_external.string().nullable(),
|
|
18550
|
+
origin: exports_external.enum(["agent", "operator"]).optional()
|
|
18550
18551
|
});
|
|
18551
18552
|
var OkApprovalLookupResponseSchema = exports_external.object({
|
|
18552
18553
|
ok: exports_external.literal(true),
|
|
@@ -18710,6 +18711,7 @@ function migrateApprovalSchema(db) {
|
|
|
18710
18711
|
var DEFAULT_MAX_TTL_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000;
|
|
18711
18712
|
|
|
18712
18713
|
// src/vault/approvals/kernel.ts
|
|
18714
|
+
var ALLOW_ONCE_CONSUMED_REASON = "allow_once_consumed";
|
|
18713
18715
|
function generateRequestId() {
|
|
18714
18716
|
return randomBytes(16).toString("hex");
|
|
18715
18717
|
}
|
|
@@ -18857,6 +18859,38 @@ function evaluateDecisionRow(db, row, currentCanonical, opts, now) {
|
|
|
18857
18859
|
if (decision.decision === "deny") {
|
|
18858
18860
|
return { state: "denied", decision };
|
|
18859
18861
|
}
|
|
18862
|
+
if (decision.decision === "allow_once") {
|
|
18863
|
+
const burn = db.run(`UPDATE approval_decisions
|
|
18864
|
+
SET last_used_at = ?, revoked_at = ?, revoke_reason = ?
|
|
18865
|
+
WHERE id = ? AND revoked_at IS NULL`, [now, now, ALLOW_ONCE_CONSUMED_REASON, decision.id]);
|
|
18866
|
+
if ((burn.changes ?? 0) === 0) {
|
|
18867
|
+
audit(db, "deny", {
|
|
18868
|
+
agent_unit: decision.agent_unit,
|
|
18869
|
+
scope: decision.scope,
|
|
18870
|
+
action: decision.action,
|
|
18871
|
+
decision_id: decision.id,
|
|
18872
|
+
context: { reason: ALLOW_ONCE_CONSUMED_REASON }
|
|
18873
|
+
});
|
|
18874
|
+
return { state: "denied", decision };
|
|
18875
|
+
}
|
|
18876
|
+
decision.last_used_at = now;
|
|
18877
|
+
decision.revoked_at = now;
|
|
18878
|
+
decision.revoke_reason = ALLOW_ONCE_CONSUMED_REASON;
|
|
18879
|
+
audit(db, "match", {
|
|
18880
|
+
agent_unit: decision.agent_unit,
|
|
18881
|
+
scope: decision.scope,
|
|
18882
|
+
action: decision.action,
|
|
18883
|
+
decision_id: decision.id
|
|
18884
|
+
});
|
|
18885
|
+
audit(db, "revoke", {
|
|
18886
|
+
agent_unit: decision.agent_unit,
|
|
18887
|
+
scope: decision.scope,
|
|
18888
|
+
action: decision.action,
|
|
18889
|
+
decision_id: decision.id,
|
|
18890
|
+
context: { actor: "kernel", reason: ALLOW_ONCE_CONSUMED_REASON }
|
|
18891
|
+
});
|
|
18892
|
+
return { state: "granted", decision };
|
|
18893
|
+
}
|
|
18860
18894
|
if (decision.decision === "allow_ttl") {
|
|
18861
18895
|
const maxLifetime = opts.max_ttl_lifetime_ms ?? DEFAULT_MAX_TTL_LIFETIME_MS;
|
|
18862
18896
|
const hardCap = decision.granted_at + maxLifetime;
|
|
@@ -18925,7 +18959,8 @@ function consumeNonce(db, request_id, now = Date.now()) {
|
|
|
18925
18959
|
}
|
|
18926
18960
|
function recordDecision(db, input, now = Date.now()) {
|
|
18927
18961
|
const id = randomUUID();
|
|
18928
|
-
const
|
|
18962
|
+
const maxLifetime = input.max_ttl_lifetime_ms ?? DEFAULT_MAX_TTL_LIFETIME_MS;
|
|
18963
|
+
const ttl_expires_at = input.decision === "allow_ttl" && input.ttl_ms ? now + Math.min(input.ttl_ms, maxLifetime) : null;
|
|
18929
18964
|
const canonical = canonicalizeApproverSet(input.approver_set);
|
|
18930
18965
|
db.run(`INSERT INTO approval_decisions
|
|
18931
18966
|
(id, agent_unit, scope, action, decision,
|
|
@@ -19123,6 +19158,7 @@ var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`
|
|
|
19123
19158
|
var HINDSIGHT_CREDS_MIRROR_VOLUME = `consumer-creds-${HINDSIGHT_CONSUMER_NAME}`;
|
|
19124
19159
|
var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
|
|
19125
19160
|
var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
|
|
19161
|
+
var DOCKER_PROBE_TIMEOUT_MS = 60 * 1000;
|
|
19126
19162
|
|
|
19127
19163
|
// src/memory/hindsight.ts
|
|
19128
19164
|
var DEFAULT_RETAIN_MISSION = "Extract durable facts that will still be true and useful weeks from now: " + "user preferences and standing rules, ongoing projects and recurring " + "commitments, technical and architectural decisions with their rationale, " + "and people/tool relationships. A preference revealed by a request is " + "durable — record the preference (what the user likes, wants, or always " + `does), not the request itself.
|
|
@@ -19385,6 +19421,9 @@ function handleConnection(socket, agent, db, isOperator = false) {
|
|
|
19385
19421
|
}
|
|
19386
19422
|
var KERNEL_OPERATOR_NAME = "operator";
|
|
19387
19423
|
var OPERATOR_ALLOWED_OPS = new Set(["approval_list"]);
|
|
19424
|
+
function listenerOrigin(isOperator) {
|
|
19425
|
+
return isOperator ? "operator" : "agent";
|
|
19426
|
+
}
|
|
19388
19427
|
function handleRequest(socket, req, agent, db, peerUid, isOperator = false) {
|
|
19389
19428
|
if (isOperator && !OPERATOR_ALLOWED_OPS.has(req.op)) {
|
|
19390
19429
|
socket.write(encodeResponse(errorResponse("DENIED", `kernel operator socket is read-only: '${req.op}' is not permitted (only ${[...OPERATOR_ALLOWED_OPS].join(", ")})`)));
|
|
@@ -19445,7 +19484,8 @@ function handleRequest(socket, req, agent, db, peerUid, isOperator = false) {
|
|
|
19445
19484
|
ttl_expires_at: r.decision.ttl_expires_at,
|
|
19446
19485
|
last_used_at: r.decision.last_used_at,
|
|
19447
19486
|
revoked_at: r.decision.revoked_at,
|
|
19448
|
-
revoke_reason: r.decision.revoke_reason
|
|
19487
|
+
revoke_reason: r.decision.revoke_reason,
|
|
19488
|
+
origin: r.decision.origin
|
|
19449
19489
|
} : null;
|
|
19450
19490
|
socket.write(encodeResponse({ ok: true, state: r.state, decision }));
|
|
19451
19491
|
return;
|
|
@@ -19472,7 +19512,8 @@ function handleRequest(socket, req, agent, db, peerUid, isOperator = false) {
|
|
|
19472
19512
|
ttl_expires_at: r.decision.ttl_expires_at,
|
|
19473
19513
|
last_used_at: r.decision.last_used_at,
|
|
19474
19514
|
revoked_at: r.decision.revoked_at,
|
|
19475
|
-
revoke_reason: r.decision.revoke_reason
|
|
19515
|
+
revoke_reason: r.decision.revoke_reason,
|
|
19516
|
+
origin: r.decision.origin
|
|
19476
19517
|
} : null;
|
|
19477
19518
|
socket.write(encodeResponse({ ok: true, state: r.state, decision }));
|
|
19478
19519
|
return;
|
|
@@ -19534,7 +19575,8 @@ function handleRequest(socket, req, agent, db, peerUid, isOperator = false) {
|
|
|
19534
19575
|
decision: req.decision,
|
|
19535
19576
|
approver_set: req.approver_set,
|
|
19536
19577
|
granted_by_user_id: req.granted_by_user_id,
|
|
19537
|
-
ttl_ms: req.ttl_ms ?? undefined
|
|
19578
|
+
ttl_ms: req.ttl_ms ?? undefined,
|
|
19579
|
+
origin: listenerOrigin(isOperator)
|
|
19538
19580
|
});
|
|
19539
19581
|
socket.write(encodeResponse({ ok: true, decision_id }));
|
|
19540
19582
|
return;
|
|
@@ -19553,7 +19595,8 @@ function handleRequest(socket, req, agent, db, peerUid, isOperator = false) {
|
|
|
19553
19595
|
decision: req.decision,
|
|
19554
19596
|
approver_set: req.approver_set,
|
|
19555
19597
|
granted_by_user_id: req.granted_by_user_id,
|
|
19556
|
-
ttl_ms: req.ttl_ms ?? undefined
|
|
19598
|
+
ttl_ms: req.ttl_ms ?? undefined,
|
|
19599
|
+
origin: listenerOrigin(isOperator)
|
|
19557
19600
|
});
|
|
19558
19601
|
if (!res.consumed) {
|
|
19559
19602
|
socket.write(encodeResponse({ ok: true, consumed: false }));
|
|
@@ -19571,7 +19614,16 @@ function handleRequest(socket, req, agent, db, peerUid, isOperator = false) {
|
|
|
19571
19614
|
return;
|
|
19572
19615
|
}
|
|
19573
19616
|
if (req.op === "approval_list") {
|
|
19574
|
-
|
|
19617
|
+
let listFilter = { agent_unit: req.agent_unit };
|
|
19618
|
+
if (!isOperator) {
|
|
19619
|
+
const acl = checkApprovalAclByAgent(agent, req.agent_unit ?? agent);
|
|
19620
|
+
if (!acl.allow) {
|
|
19621
|
+
socket.write(encodeResponse(errorResponse("DENIED", acl.reason)));
|
|
19622
|
+
return;
|
|
19623
|
+
}
|
|
19624
|
+
listFilter = { agent_unit: agent };
|
|
19625
|
+
}
|
|
19626
|
+
const decisions = listDecisions(db, listFilter);
|
|
19575
19627
|
const meta = decisions.map((d) => ({
|
|
19576
19628
|
id: d.id,
|
|
19577
19629
|
agent_unit: d.agent_unit,
|
|
@@ -19733,6 +19785,7 @@ if (import.meta.url === `file://${process.argv[1]}` && /(?:^|[/\\])(?:vault[/\\]
|
|
|
19733
19785
|
export {
|
|
19734
19786
|
openKernelDb,
|
|
19735
19787
|
main,
|
|
19788
|
+
listenerOrigin,
|
|
19736
19789
|
bootstrap,
|
|
19737
19790
|
KERNEL_OPERATOR_NAME
|
|
19738
19791
|
};
|
|
@@ -18222,7 +18222,7 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
18222
18222
|
|
|
18223
18223
|
// src/vault/broker/server.ts
|
|
18224
18224
|
import * as net from "node:net";
|
|
18225
|
-
import { mkdirSync as mkdirSync7, chmodSync as chmodSync5, chownSync, existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as
|
|
18225
|
+
import { mkdirSync as mkdirSync7, chmodSync as chmodSync5, chownSync, existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync7, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync8 } from "node:fs";
|
|
18226
18226
|
|
|
18227
18227
|
// src/agents/compose.ts
|
|
18228
18228
|
init_schema();
|
|
@@ -18831,6 +18831,7 @@ var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`
|
|
|
18831
18831
|
var HINDSIGHT_CREDS_MIRROR_VOLUME = `consumer-creds-${HINDSIGHT_CONSUMER_NAME}`;
|
|
18832
18832
|
var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
|
|
18833
18833
|
var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
|
|
18834
|
+
var DOCKER_PROBE_TIMEOUT_MS = 60 * 1000;
|
|
18834
18835
|
|
|
18835
18836
|
// src/memory/hindsight.ts
|
|
18836
18837
|
var DEFAULT_RETAIN_MISSION = "Extract durable facts that will still be true and useful weeks from now: " + "user preferences and standing rules, ongoing projects and recurring " + "commitments, technical and architectural decisions with their rationale, " + "and people/tool relationships. A preference revealed by a request is " + "durable — record the preference (what the user likes, wants, or always " + `does), not the request itself.
|
|
@@ -19546,7 +19547,8 @@ var ApprovalDecisionMetaSchema = exports_external.object({
|
|
|
19546
19547
|
ttl_expires_at: exports_external.number().nullable(),
|
|
19547
19548
|
last_used_at: exports_external.number().nullable(),
|
|
19548
19549
|
revoked_at: exports_external.number().nullable(),
|
|
19549
|
-
revoke_reason: exports_external.string().nullable()
|
|
19550
|
+
revoke_reason: exports_external.string().nullable(),
|
|
19551
|
+
origin: exports_external.enum(["agent", "operator"]).optional()
|
|
19550
19552
|
});
|
|
19551
19553
|
var OkApprovalLookupResponseSchema = exports_external.object({
|
|
19552
19554
|
ok: exports_external.literal(true),
|
|
@@ -19745,9 +19747,9 @@ function migrateLegacyGrantsDbLocation(newDbPath = getGrantsDbPath(), deps) {
|
|
|
19745
19747
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
19746
19748
|
|
|
19747
19749
|
// src/vault/broker/server.ts
|
|
19748
|
-
import { dirname as dirname7, resolve as resolve9, basename as
|
|
19750
|
+
import { dirname as dirname7, resolve as resolve9, basename as basename5 } from "node:path";
|
|
19749
19751
|
import * as os3 from "node:os";
|
|
19750
|
-
import * as
|
|
19752
|
+
import * as path5 from "node:path";
|
|
19751
19753
|
|
|
19752
19754
|
// src/vault/migrate-layout.ts
|
|
19753
19755
|
import {
|
|
@@ -19981,9 +19983,9 @@ function readAutoUnlockFile(filePath) {
|
|
|
19981
19983
|
var DEFAULT_AUTO_UNLOCK_PATH = "~/.switchroom/vault-auto-unlock";
|
|
19982
19984
|
|
|
19983
19985
|
// src/vault/broker/audit-log.ts
|
|
19984
|
-
import * as
|
|
19986
|
+
import * as fs3 from "node:fs";
|
|
19985
19987
|
import * as os2 from "node:os";
|
|
19986
|
-
import * as
|
|
19988
|
+
import * as path3 from "node:path";
|
|
19987
19989
|
|
|
19988
19990
|
// src/util/audit-hashchain.ts
|
|
19989
19991
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -20100,76 +20102,107 @@ function safeAuditLogPath(requestedPath) {
|
|
|
20100
20102
|
return cachedTmpAuditLog;
|
|
20101
20103
|
}
|
|
20102
20104
|
|
|
20103
|
-
// src/
|
|
20104
|
-
|
|
20105
|
-
|
|
20106
|
-
|
|
20107
|
-
|
|
20105
|
+
// src/util/log-rotation.ts
|
|
20106
|
+
import * as fs2 from "node:fs";
|
|
20107
|
+
import * as path2 from "node:path";
|
|
20108
|
+
function fsyncAndMeasure(snapshotPath, logPath, tag) {
|
|
20109
|
+
let step = "open";
|
|
20108
20110
|
try {
|
|
20109
|
-
const
|
|
20110
|
-
|
|
20111
|
-
|
|
20112
|
-
|
|
20113
|
-
|
|
20114
|
-
|
|
20115
|
-
|
|
20116
|
-
|
|
20117
|
-
|
|
20118
|
-
|
|
20111
|
+
const fd = fs2.openSync(snapshotPath, "r");
|
|
20112
|
+
try {
|
|
20113
|
+
step = "fsync";
|
|
20114
|
+
fs2.fsyncSync(fd);
|
|
20115
|
+
step = "measure";
|
|
20116
|
+
const size = fs2.fstatSync(fd).size;
|
|
20117
|
+
step = "close";
|
|
20118
|
+
return size;
|
|
20119
|
+
} finally {
|
|
20120
|
+
fs2.closeSync(fd);
|
|
20121
|
+
}
|
|
20122
|
+
} catch (err) {
|
|
20123
|
+
process.stderr.write(`[${tag}] ERROR: could not ${step} snapshot ${snapshotPath}; leaving active log ${logPath} intact to avoid data loss: ${err.message}
|
|
20124
|
+
`);
|
|
20125
|
+
return null;
|
|
20119
20126
|
}
|
|
20120
20127
|
}
|
|
20121
|
-
|
|
20122
|
-
|
|
20123
|
-
|
|
20124
|
-
|
|
20125
|
-
|
|
20126
|
-
|
|
20127
|
-
|
|
20128
|
-
|
|
20129
|
-
}
|
|
20130
|
-
|
|
20131
|
-
|
|
20128
|
+
function rotateLogFile(logPath, maxFiles, tag, stillHeld) {
|
|
20129
|
+
const keep = Math.max(1, Math.floor(maxFiles));
|
|
20130
|
+
const held = (what) => {
|
|
20131
|
+
if (!stillHeld || stillHeld())
|
|
20132
|
+
return true;
|
|
20133
|
+
process.stderr.write(`[${tag}] WARN: rotation lock for ${logPath} was reclaimed while we were inside it; declining to ${what}
|
|
20134
|
+
`);
|
|
20135
|
+
return false;
|
|
20136
|
+
};
|
|
20137
|
+
let entryBytes;
|
|
20138
|
+
try {
|
|
20139
|
+
entryBytes = fs2.statSync(logPath).size;
|
|
20140
|
+
} catch (err) {
|
|
20141
|
+
process.stderr.write(`[${tag}] ERROR: could not stat active log ${logPath} before rotating it: ${err.message}
|
|
20142
|
+
`);
|
|
20143
|
+
return false;
|
|
20144
|
+
}
|
|
20145
|
+
const notRotatedUnderUs = (what) => {
|
|
20146
|
+
let live;
|
|
20147
|
+
try {
|
|
20148
|
+
live = fs2.statSync(logPath).size;
|
|
20149
|
+
} catch (err) {
|
|
20150
|
+
process.stderr.write(`[${tag}] ERROR: could not re-stat active log ${logPath}; declining to ${what}: ${err.message}
|
|
20151
|
+
`);
|
|
20152
|
+
return false;
|
|
20153
|
+
}
|
|
20154
|
+
if (live < entryBytes) {
|
|
20155
|
+
process.stderr.write(`[${tag}] WARN: active log ${logPath} shrank from ${entryBytes} to ${live} bytes while we were rotating it; another rotator snapshotted it and these generations are its, not ours — declining to ${what}
|
|
20156
|
+
`);
|
|
20157
|
+
return false;
|
|
20158
|
+
}
|
|
20159
|
+
return true;
|
|
20160
|
+
};
|
|
20161
|
+
const oldest = `${logPath}.${keep}`;
|
|
20132
20162
|
if (fs2.existsSync(oldest)) {
|
|
20163
|
+
if (!held(`drop ${oldest}`))
|
|
20164
|
+
return false;
|
|
20165
|
+
if (!notRotatedUnderUs(`drop ${oldest}`))
|
|
20166
|
+
return false;
|
|
20133
20167
|
try {
|
|
20134
20168
|
fs2.unlinkSync(oldest);
|
|
20135
20169
|
} catch (err) {
|
|
20136
|
-
process.stderr.write(`[
|
|
20170
|
+
process.stderr.write(`[${tag}] ERROR: could not drop oldest rotation ${oldest}: ${err.message}
|
|
20137
20171
|
`);
|
|
20138
20172
|
}
|
|
20139
20173
|
}
|
|
20140
|
-
for (let n =
|
|
20174
|
+
for (let n = keep - 1;n >= 1; n--) {
|
|
20141
20175
|
const from = `${logPath}.${n}`;
|
|
20142
20176
|
const to = `${logPath}.${n + 1}`;
|
|
20143
20177
|
if (!fs2.existsSync(from))
|
|
20144
20178
|
continue;
|
|
20179
|
+
if (!held(`shift ${from} → ${to}`))
|
|
20180
|
+
return false;
|
|
20181
|
+
if (!notRotatedUnderUs(`shift ${from} → ${to}`))
|
|
20182
|
+
return false;
|
|
20145
20183
|
try {
|
|
20146
20184
|
fs2.renameSync(from, to);
|
|
20147
20185
|
} catch (err) {
|
|
20148
|
-
process.stderr.write(`[
|
|
20186
|
+
process.stderr.write(`[${tag}] ERROR: could not rotate ${from} → ${to}: ${err.message}
|
|
20149
20187
|
`);
|
|
20150
|
-
return;
|
|
20188
|
+
return false;
|
|
20151
20189
|
}
|
|
20152
20190
|
}
|
|
20153
20191
|
const snapshotPath = `${logPath}.1`;
|
|
20192
|
+
if (!held(`overwrite ${snapshotPath}`))
|
|
20193
|
+
return false;
|
|
20194
|
+
if (!notRotatedUnderUs(`overwrite ${snapshotPath}`))
|
|
20195
|
+
return false;
|
|
20154
20196
|
try {
|
|
20155
20197
|
fs2.copyFileSync(logPath, snapshotPath);
|
|
20156
20198
|
} catch (err) {
|
|
20157
|
-
process.stderr.write(`[
|
|
20199
|
+
process.stderr.write(`[${tag}] ERROR: could not snapshot active log ${logPath} → ${snapshotPath}: ${err.message}
|
|
20158
20200
|
`);
|
|
20159
|
-
return;
|
|
20160
|
-
}
|
|
20161
|
-
try {
|
|
20162
|
-
const fd = fs2.openSync(snapshotPath, "r");
|
|
20163
|
-
try {
|
|
20164
|
-
fs2.fsyncSync(fd);
|
|
20165
|
-
} finally {
|
|
20166
|
-
fs2.closeSync(fd);
|
|
20167
|
-
}
|
|
20168
|
-
} catch (err) {
|
|
20169
|
-
process.stderr.write(`[vault-audit] ERROR: could not fsync audit snapshot ${snapshotPath}; leaving active log intact to avoid data loss: ${err.message}
|
|
20170
|
-
`);
|
|
20171
|
-
return;
|
|
20201
|
+
return false;
|
|
20172
20202
|
}
|
|
20203
|
+
const snapshotBytes = fsyncAndMeasure(snapshotPath, logPath, tag);
|
|
20204
|
+
if (snapshotBytes === null)
|
|
20205
|
+
return false;
|
|
20173
20206
|
try {
|
|
20174
20207
|
const dirFd = fs2.openSync(path2.dirname(snapshotPath), "r");
|
|
20175
20208
|
try {
|
|
@@ -20178,15 +20211,78 @@ function rotateAuditLog(logPath, maxFiles) {
|
|
|
20178
20211
|
fs2.closeSync(dirFd);
|
|
20179
20212
|
}
|
|
20180
20213
|
} catch {}
|
|
20214
|
+
if (!held(`truncate ${logPath}`))
|
|
20215
|
+
return false;
|
|
20216
|
+
let liveBytes;
|
|
20217
|
+
try {
|
|
20218
|
+
liveBytes = fs2.statSync(logPath).size;
|
|
20219
|
+
} catch (err) {
|
|
20220
|
+
process.stderr.write(`[${tag}] ERROR: could not re-stat active log ${logPath} before truncating; leaving it intact to avoid data loss: ${err.message}
|
|
20221
|
+
`);
|
|
20222
|
+
return false;
|
|
20223
|
+
}
|
|
20224
|
+
if (liveBytes < snapshotBytes) {
|
|
20225
|
+
process.stderr.write(`[${tag}] WARN: active log ${logPath} shrank from ${snapshotBytes} to ${liveBytes} bytes after we snapshotted it; another rotator truncated it and these rows are its, not ours — declining to truncate
|
|
20226
|
+
`);
|
|
20227
|
+
return false;
|
|
20228
|
+
}
|
|
20181
20229
|
try {
|
|
20182
20230
|
fs2.truncateSync(logPath, 0);
|
|
20183
20231
|
} catch (err) {
|
|
20184
|
-
process.stderr.write(`[
|
|
20232
|
+
process.stderr.write(`[${tag}] ERROR: could not truncate active log ${logPath}: ${err.message}
|
|
20233
|
+
`);
|
|
20234
|
+
return false;
|
|
20235
|
+
}
|
|
20236
|
+
if (stillHeld && !stillHeld()) {
|
|
20237
|
+
process.stderr.write(`[${tag}] ERROR: truncated ${logPath} after our rotation lock was reclaimed; rows another rotator wrote between our size check and our truncate are LOST
|
|
20185
20238
|
`);
|
|
20186
20239
|
}
|
|
20240
|
+
return true;
|
|
20241
|
+
}
|
|
20242
|
+
function resolveRotationConfig(args) {
|
|
20243
|
+
const env = args.env ?? process.env;
|
|
20244
|
+
const envBytes = Number(env[args.envBytesVar]);
|
|
20245
|
+
const envFiles = Number(env[args.envFilesVar]);
|
|
20246
|
+
const maxBytes = args.maxBytes !== undefined && args.maxBytes !== 0 ? args.maxBytes : Number.isFinite(envBytes) && envBytes !== 0 ? envBytes : args.defaultBytes;
|
|
20247
|
+
const maxFiles = args.maxFiles !== undefined && args.maxFiles > 0 ? args.maxFiles : Number.isFinite(envFiles) && envFiles > 0 ? envFiles : args.defaultFiles;
|
|
20248
|
+
return { maxBytes, maxFiles };
|
|
20249
|
+
}
|
|
20250
|
+
|
|
20251
|
+
// src/vault/broker/audit-log.ts
|
|
20252
|
+
function failOpenStatePath(logPath) {
|
|
20253
|
+
return `${logPath}.failopen`;
|
|
20254
|
+
}
|
|
20255
|
+
function readFailOpenState(statePath) {
|
|
20256
|
+
try {
|
|
20257
|
+
const raw = fs3.readFileSync(statePath, "utf8");
|
|
20258
|
+
const parsed = JSON.parse(raw);
|
|
20259
|
+
const count = Number(parsed.failOpenCount);
|
|
20260
|
+
return {
|
|
20261
|
+
failOpenCount: Number.isFinite(count) && count > 0 ? count : 0,
|
|
20262
|
+
lastFailureTs: typeof parsed.lastFailureTs === "string" ? parsed.lastFailureTs : undefined,
|
|
20263
|
+
lastError: typeof parsed.lastError === "string" ? parsed.lastError : undefined
|
|
20264
|
+
};
|
|
20265
|
+
} catch {
|
|
20266
|
+
return { failOpenCount: 0 };
|
|
20267
|
+
}
|
|
20268
|
+
}
|
|
20269
|
+
var DEFAULT_AUDIT_MAX_BYTES = 32 * 1024 * 1024;
|
|
20270
|
+
var DEFAULT_AUDIT_MAX_FILES = 5;
|
|
20271
|
+
function resolveRotation(opts) {
|
|
20272
|
+
return resolveRotationConfig({
|
|
20273
|
+
maxBytes: opts.maxBytes,
|
|
20274
|
+
maxFiles: opts.maxFiles,
|
|
20275
|
+
envBytesVar: "SWITCHROOM_VAULT_AUDIT_MAX_BYTES",
|
|
20276
|
+
envFilesVar: "SWITCHROOM_VAULT_AUDIT_MAX_FILES",
|
|
20277
|
+
defaultBytes: DEFAULT_AUDIT_MAX_BYTES,
|
|
20278
|
+
defaultFiles: DEFAULT_AUDIT_MAX_FILES
|
|
20279
|
+
});
|
|
20280
|
+
}
|
|
20281
|
+
function rotateAuditLog(logPath, maxFiles) {
|
|
20282
|
+
rotateLogFile(logPath, maxFiles, "vault-audit");
|
|
20187
20283
|
}
|
|
20188
20284
|
function defaultAuditLogPath() {
|
|
20189
|
-
return
|
|
20285
|
+
return path3.join(os2.homedir(), ".switchroom", "vault-audit.log");
|
|
20190
20286
|
}
|
|
20191
20287
|
function callerFromPeer(peer) {
|
|
20192
20288
|
if (peer.systemdUnit !== null && peer.systemdUnit.length > 0) {
|
|
@@ -20208,7 +20304,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20208
20304
|
lastError: String(err.message).slice(0, 300)
|
|
20209
20305
|
};
|
|
20210
20306
|
try {
|
|
20211
|
-
|
|
20307
|
+
fs3.writeFileSync(statePath, `${JSON.stringify(state)}
|
|
20212
20308
|
`, { mode: 384 });
|
|
20213
20309
|
} catch (persistErr) {
|
|
20214
20310
|
process.stderr.write(`[vault-audit] ERROR: could not persist fail-open counter ${statePath}: ${persistErr.message}
|
|
@@ -20222,7 +20318,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20222
20318
|
write(entry) {
|
|
20223
20319
|
if (rotation.maxBytes > 0) {
|
|
20224
20320
|
try {
|
|
20225
|
-
const size =
|
|
20321
|
+
const size = fs3.statSync(logPath).size;
|
|
20226
20322
|
if (size >= rotation.maxBytes) {
|
|
20227
20323
|
rotateAuditLog(logPath, rotation.maxFiles);
|
|
20228
20324
|
}
|
|
@@ -20231,7 +20327,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20231
20327
|
const { line, next } = chainRow(chain, entry);
|
|
20232
20328
|
let fd;
|
|
20233
20329
|
try {
|
|
20234
|
-
fd =
|
|
20330
|
+
fd = fs3.openSync(logPath, "a", 384);
|
|
20235
20331
|
} catch (err) {
|
|
20236
20332
|
process.stderr.write(`[vault-audit] ERROR: could not open audit log ${logPath}: ${err.message}
|
|
20237
20333
|
`);
|
|
@@ -20240,7 +20336,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20240
20336
|
}
|
|
20241
20337
|
let durable = false;
|
|
20242
20338
|
try {
|
|
20243
|
-
|
|
20339
|
+
fs3.writeSync(fd, line);
|
|
20244
20340
|
chain = next;
|
|
20245
20341
|
durable = true;
|
|
20246
20342
|
} catch (err) {
|
|
@@ -20249,7 +20345,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20249
20345
|
recordFailOpen(err);
|
|
20250
20346
|
} finally {
|
|
20251
20347
|
try {
|
|
20252
|
-
|
|
20348
|
+
fs3.closeSync(fd);
|
|
20253
20349
|
} catch (closeErr) {
|
|
20254
20350
|
process.stderr.write(`[vault-audit] ERROR: could not close audit log fd: ${closeErr.message}
|
|
20255
20351
|
`);
|
|
@@ -22168,8 +22264,8 @@ function adminOnlyKeysBeingAdded(requestedKeys, existingKeys, patterns) {
|
|
|
22168
22264
|
}
|
|
22169
22265
|
|
|
22170
22266
|
// src/vault/grants-db.ts
|
|
22171
|
-
import * as
|
|
22172
|
-
import * as
|
|
22267
|
+
import * as path4 from "node:path";
|
|
22268
|
+
import * as fs4 from "node:fs";
|
|
22173
22269
|
import { Database } from "bun:sqlite";
|
|
22174
22270
|
|
|
22175
22271
|
// src/vault/approvals/schema.ts
|
|
@@ -22262,12 +22358,12 @@ function isGrantsDbCorruption(err) {
|
|
|
22262
22358
|
function quarantineGrantsDb(dbPath) {
|
|
22263
22359
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
22264
22360
|
const quarantinePath = `${dbPath}.corrupt-${stamp}`;
|
|
22265
|
-
|
|
22361
|
+
fs4.renameSync(dbPath, quarantinePath);
|
|
22266
22362
|
for (const suffix of WAL_SIDECAR_SUFFIXES2) {
|
|
22267
22363
|
const sidecar = `${dbPath}${suffix}`;
|
|
22268
|
-
if (
|
|
22364
|
+
if (fs4.existsSync(sidecar)) {
|
|
22269
22365
|
try {
|
|
22270
|
-
|
|
22366
|
+
fs4.renameSync(sidecar, `${quarantinePath}${suffix}`);
|
|
22271
22367
|
} catch {}
|
|
22272
22368
|
}
|
|
22273
22369
|
}
|
|
@@ -22277,7 +22373,7 @@ function openAndMigrate(dbPath) {
|
|
|
22277
22373
|
const db = new Database(dbPath, { create: true });
|
|
22278
22374
|
try {
|
|
22279
22375
|
try {
|
|
22280
|
-
|
|
22376
|
+
fs4.chmodSync(dbPath, 384);
|
|
22281
22377
|
} catch {}
|
|
22282
22378
|
db.run("PRAGMA journal_mode=WAL");
|
|
22283
22379
|
db.run("PRAGMA busy_timeout=5000");
|
|
@@ -22293,8 +22389,8 @@ function openAndMigrate(dbPath) {
|
|
|
22293
22389
|
return db;
|
|
22294
22390
|
}
|
|
22295
22391
|
function openGrantsDb(dbPath = DEFAULT_GRANTS_DB_PATH, opener = openAndMigrate) {
|
|
22296
|
-
const dir =
|
|
22297
|
-
|
|
22392
|
+
const dir = path4.dirname(dbPath);
|
|
22393
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
22298
22394
|
migrateLegacyGrantsDbLocation(dbPath);
|
|
22299
22395
|
try {
|
|
22300
22396
|
return opener(dbPath);
|
|
@@ -22318,6 +22414,7 @@ function canonicalizeApproverSet(approvers) {
|
|
|
22318
22414
|
}
|
|
22319
22415
|
|
|
22320
22416
|
// src/vault/approvals/kernel.ts
|
|
22417
|
+
var ALLOW_ONCE_CONSUMED_REASON = "allow_once_consumed";
|
|
22321
22418
|
function generateRequestId() {
|
|
22322
22419
|
return randomBytes5(16).toString("hex");
|
|
22323
22420
|
}
|
|
@@ -22444,6 +22541,38 @@ function evaluateDecisionRow(db, row, currentCanonical, opts, now) {
|
|
|
22444
22541
|
if (decision.decision === "deny") {
|
|
22445
22542
|
return { state: "denied", decision };
|
|
22446
22543
|
}
|
|
22544
|
+
if (decision.decision === "allow_once") {
|
|
22545
|
+
const burn = db.run(`UPDATE approval_decisions
|
|
22546
|
+
SET last_used_at = ?, revoked_at = ?, revoke_reason = ?
|
|
22547
|
+
WHERE id = ? AND revoked_at IS NULL`, [now, now, ALLOW_ONCE_CONSUMED_REASON, decision.id]);
|
|
22548
|
+
if ((burn.changes ?? 0) === 0) {
|
|
22549
|
+
audit(db, "deny", {
|
|
22550
|
+
agent_unit: decision.agent_unit,
|
|
22551
|
+
scope: decision.scope,
|
|
22552
|
+
action: decision.action,
|
|
22553
|
+
decision_id: decision.id,
|
|
22554
|
+
context: { reason: ALLOW_ONCE_CONSUMED_REASON }
|
|
22555
|
+
});
|
|
22556
|
+
return { state: "denied", decision };
|
|
22557
|
+
}
|
|
22558
|
+
decision.last_used_at = now;
|
|
22559
|
+
decision.revoked_at = now;
|
|
22560
|
+
decision.revoke_reason = ALLOW_ONCE_CONSUMED_REASON;
|
|
22561
|
+
audit(db, "match", {
|
|
22562
|
+
agent_unit: decision.agent_unit,
|
|
22563
|
+
scope: decision.scope,
|
|
22564
|
+
action: decision.action,
|
|
22565
|
+
decision_id: decision.id
|
|
22566
|
+
});
|
|
22567
|
+
audit(db, "revoke", {
|
|
22568
|
+
agent_unit: decision.agent_unit,
|
|
22569
|
+
scope: decision.scope,
|
|
22570
|
+
action: decision.action,
|
|
22571
|
+
decision_id: decision.id,
|
|
22572
|
+
context: { actor: "kernel", reason: ALLOW_ONCE_CONSUMED_REASON }
|
|
22573
|
+
});
|
|
22574
|
+
return { state: "granted", decision };
|
|
22575
|
+
}
|
|
22447
22576
|
if (decision.decision === "allow_ttl") {
|
|
22448
22577
|
const maxLifetime = opts.max_ttl_lifetime_ms ?? DEFAULT_MAX_TTL_LIFETIME_MS;
|
|
22449
22578
|
const hardCap = decision.granted_at + maxLifetime;
|
|
@@ -22512,7 +22641,8 @@ function consumeNonce(db, request_id, now = Date.now()) {
|
|
|
22512
22641
|
}
|
|
22513
22642
|
function recordDecision(db, input, now = Date.now()) {
|
|
22514
22643
|
const id = randomUUID();
|
|
22515
|
-
const
|
|
22644
|
+
const maxLifetime = input.max_ttl_lifetime_ms ?? DEFAULT_MAX_TTL_LIFETIME_MS;
|
|
22645
|
+
const ttl_expires_at = input.decision === "allow_ttl" && input.ttl_ms ? now + Math.min(input.ttl_ms, maxLifetime) : null;
|
|
22516
22646
|
const canonical = canonicalizeApproverSet(input.approver_set);
|
|
22517
22647
|
db.run(`INSERT INTO approval_decisions
|
|
22518
22648
|
(id, agent_unit, scope, action, decision,
|
|
@@ -23827,10 +23957,10 @@ class VaultBroker {
|
|
|
23827
23957
|
return;
|
|
23828
23958
|
}
|
|
23829
23959
|
try {
|
|
23830
|
-
const agentsDir = this.config ? resolveAgentsDir(this.config) :
|
|
23831
|
-
const tokenDir =
|
|
23960
|
+
const agentsDir = this.config ? resolveAgentsDir(this.config) : path5.join(os3.homedir(), ".switchroom", "agents");
|
|
23961
|
+
const tokenDir = path5.join(agentsDir, agent);
|
|
23832
23962
|
mkdirSync7(tokenDir, { recursive: true });
|
|
23833
|
-
const tokenPath =
|
|
23963
|
+
const tokenPath = path5.join(tokenDir, ".vault-token");
|
|
23834
23964
|
const tmpPath = `${tokenPath}.tmp.${process.pid}`;
|
|
23835
23965
|
writeFileSync3(tmpPath, mintResult.token, { mode: 384 });
|
|
23836
23966
|
renameSync8(tmpPath, tokenPath);
|
|
@@ -23892,8 +24022,8 @@ class VaultBroker {
|
|
|
23892
24022
|
try {
|
|
23893
24023
|
const row = this.grantsDb.query("SELECT agent_slug FROM vault_grants WHERE id = ?").get(id);
|
|
23894
24024
|
if (row && AgentNameSchema.safeParse(row.agent_slug).success) {
|
|
23895
|
-
const agentsDir = this.config ? resolveAgentsDir(this.config) :
|
|
23896
|
-
const tokenPath =
|
|
24025
|
+
const agentsDir = this.config ? resolveAgentsDir(this.config) : path5.join(os3.homedir(), ".switchroom", "agents");
|
|
24026
|
+
const tokenPath = path5.join(agentsDir, row.agent_slug, ".vault-token");
|
|
23897
24027
|
if (existsSync12(tokenPath)) {
|
|
23898
24028
|
try {
|
|
23899
24029
|
unlinkSync6(tokenPath);
|
|
@@ -24135,7 +24265,7 @@ class VaultBroker {
|
|
|
24135
24265
|
if (!existsSync12(filePath))
|
|
24136
24266
|
return false;
|
|
24137
24267
|
try {
|
|
24138
|
-
if (
|
|
24268
|
+
if (statSync7(filePath).size === 0)
|
|
24139
24269
|
return false;
|
|
24140
24270
|
} catch {
|
|
24141
24271
|
return false;
|
|
@@ -24214,12 +24344,12 @@ class VaultBroker {
|
|
|
24214
24344
|
}
|
|
24215
24345
|
function detectVaultLayoutDrift(vaultPath) {
|
|
24216
24346
|
const dir = dirname7(vaultPath);
|
|
24217
|
-
if (
|
|
24347
|
+
if (basename5(dir) !== "vault")
|
|
24218
24348
|
return;
|
|
24219
|
-
if (
|
|
24349
|
+
if (basename5(vaultPath) !== "vault.enc")
|
|
24220
24350
|
return;
|
|
24221
24351
|
const switchroomDir = dirname7(dir);
|
|
24222
|
-
if (
|
|
24352
|
+
if (basename5(switchroomDir) !== ".switchroom")
|
|
24223
24353
|
return;
|
|
24224
24354
|
const home2 = dirname7(switchroomDir);
|
|
24225
24355
|
const result = inspectVaultLayout(home2);
|
|
@@ -24248,7 +24378,7 @@ async function main() {
|
|
|
24248
24378
|
let perAgentTargets = [];
|
|
24249
24379
|
try {
|
|
24250
24380
|
if (existsSync12(perAgentDir)) {
|
|
24251
|
-
const entries =
|
|
24381
|
+
const entries = readdirSync5(perAgentDir, { withFileTypes: true });
|
|
24252
24382
|
const flat = [];
|
|
24253
24383
|
const subdirs = [];
|
|
24254
24384
|
for (const e of entries) {
|