switchroom 0.12.13 → 0.12.15
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/README.md +107 -371
- package/dist/cli/switchroom.js +627 -386
- package/dist/vault/approvals/kernel-server.js +88 -1
- package/dist/vault/broker/server.js +132 -2
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +289 -81
- package/telegram-plugin/gateway/approval-callback.test.ts +49 -1
- package/telegram-plugin/gateway/approval-callback.ts +85 -56
- package/telegram-plugin/gateway/gateway.ts +168 -19
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +39 -0
- package/telegram-plugin/gateway/pending-permission-decisions.ts +112 -0
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +117 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +71 -1
- package/telegram-plugin/tests/pending-permission-decisions.test.ts +73 -0
- package/telegram-plugin/tests/vault-save-inbound-builders.test.ts +96 -0
|
@@ -11719,6 +11719,24 @@ var LockRequestSchema = exports_external.object({
|
|
|
11719
11719
|
v: exports_external.literal(1),
|
|
11720
11720
|
op: exports_external.literal("lock")
|
|
11721
11721
|
});
|
|
11722
|
+
var PreflightAccessRequestSchema = exports_external.object({
|
|
11723
|
+
v: exports_external.literal(1),
|
|
11724
|
+
op: exports_external.literal("preflight_access"),
|
|
11725
|
+
agent: exports_external.string().min(1),
|
|
11726
|
+
keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
|
|
11727
|
+
});
|
|
11728
|
+
var OkPreflightAccessResponseSchema = exports_external.object({
|
|
11729
|
+
ok: exports_external.literal(true),
|
|
11730
|
+
op: exports_external.literal("preflight_access"),
|
|
11731
|
+
results: exports_external.array(exports_external.object({
|
|
11732
|
+
key: exports_external.string(),
|
|
11733
|
+
exists: exports_external.boolean(),
|
|
11734
|
+
acl_ok: exports_external.boolean(),
|
|
11735
|
+
acl_reason: exports_external.string().optional(),
|
|
11736
|
+
scope_ok: exports_external.boolean(),
|
|
11737
|
+
scope_reason: exports_external.string().optional()
|
|
11738
|
+
}))
|
|
11739
|
+
});
|
|
11722
11740
|
var ApprovalRequestRequestSchema = exports_external.object({
|
|
11723
11741
|
v: exports_external.literal(1),
|
|
11724
11742
|
op: exports_external.literal("approval_request"),
|
|
@@ -11770,12 +11788,22 @@ var ApprovalRecordRequestSchema = exports_external.object({
|
|
|
11770
11788
|
granted_by_user_id: exports_external.number().int(),
|
|
11771
11789
|
ttl_ms: exports_external.number().int().positive().nullable().optional()
|
|
11772
11790
|
});
|
|
11791
|
+
var ApprovalConsumeRecordRequestSchema = exports_external.object({
|
|
11792
|
+
v: exports_external.literal(1),
|
|
11793
|
+
op: exports_external.literal("approval_consume_record"),
|
|
11794
|
+
request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
|
|
11795
|
+
decision: ApprovalDecisionModeSchema,
|
|
11796
|
+
approver_set: exports_external.array(exports_external.string()),
|
|
11797
|
+
granted_by_user_id: exports_external.number().int(),
|
|
11798
|
+
ttl_ms: exports_external.number().int().positive().nullable().optional()
|
|
11799
|
+
});
|
|
11773
11800
|
var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
11774
11801
|
GetRequestSchema,
|
|
11775
11802
|
PutRequestSchema,
|
|
11776
11803
|
ListRequestSchema,
|
|
11777
11804
|
StatusRequestSchema,
|
|
11778
11805
|
LockRequestSchema,
|
|
11806
|
+
PreflightAccessRequestSchema,
|
|
11779
11807
|
MintGrantRequestSchema,
|
|
11780
11808
|
ListGrantsRequestSchema,
|
|
11781
11809
|
RevokeGrantRequestSchema,
|
|
@@ -11784,7 +11812,8 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
|
11784
11812
|
ApprovalConsumeRequestSchema,
|
|
11785
11813
|
ApprovalRevokeRequestSchema,
|
|
11786
11814
|
ApprovalListRequestSchema,
|
|
11787
|
-
ApprovalRecordRequestSchema
|
|
11815
|
+
ApprovalRecordRequestSchema,
|
|
11816
|
+
ApprovalConsumeRecordRequestSchema
|
|
11788
11817
|
]);
|
|
11789
11818
|
var VaultEntrySchema = exports_external.union([
|
|
11790
11819
|
exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
|
|
@@ -11906,6 +11935,15 @@ var OkApprovalRecordResponseSchema = exports_external.object({
|
|
|
11906
11935
|
ok: exports_external.literal(true),
|
|
11907
11936
|
decision_id: exports_external.string()
|
|
11908
11937
|
});
|
|
11938
|
+
var OkApprovalConsumeRecordResponseSchema = exports_external.object({
|
|
11939
|
+
ok: exports_external.literal(true),
|
|
11940
|
+
consumed: exports_external.boolean(),
|
|
11941
|
+
decision_id: exports_external.string().optional(),
|
|
11942
|
+
agent_unit: exports_external.string().optional(),
|
|
11943
|
+
scope: exports_external.string().optional(),
|
|
11944
|
+
action: exports_external.string().optional(),
|
|
11945
|
+
why: exports_external.string().nullable().optional()
|
|
11946
|
+
});
|
|
11909
11947
|
var ErrorResponseSchema = exports_external.object({
|
|
11910
11948
|
ok: exports_external.literal(false),
|
|
11911
11949
|
code: ErrorCode,
|
|
@@ -11916,6 +11954,7 @@ var ResponseSchema = exports_external.union([
|
|
|
11916
11954
|
OkKeysResponseSchema,
|
|
11917
11955
|
OkStatusResponseSchema,
|
|
11918
11956
|
OkLockResponseSchema,
|
|
11957
|
+
OkPreflightAccessResponseSchema,
|
|
11919
11958
|
OkPutResponseSchema,
|
|
11920
11959
|
OkMintGrantResponseSchema,
|
|
11921
11960
|
OkListGrantsResponseSchema,
|
|
@@ -11926,6 +11965,7 @@ var ResponseSchema = exports_external.union([
|
|
|
11926
11965
|
OkApprovalRevokeResponseSchema,
|
|
11927
11966
|
OkApprovalListResponseSchema,
|
|
11928
11967
|
OkApprovalRecordResponseSchema,
|
|
11968
|
+
OkApprovalConsumeRecordResponseSchema,
|
|
11929
11969
|
ErrorResponseSchema
|
|
11930
11970
|
]);
|
|
11931
11971
|
function decodeRequest(line) {
|
|
@@ -12244,6 +12284,22 @@ function recordDecision(db, input, now = Date.now()) {
|
|
|
12244
12284
|
});
|
|
12245
12285
|
return id;
|
|
12246
12286
|
}
|
|
12287
|
+
function consumeAndRecord(db, input, now = Date.now()) {
|
|
12288
|
+
const run = db.transaction(() => {
|
|
12289
|
+
const nonce = consumeNonce(db, input.request_id, now);
|
|
12290
|
+
if (nonce === null)
|
|
12291
|
+
return { consumed: false };
|
|
12292
|
+
const decision_id = recordDecision(db, {
|
|
12293
|
+
nonce,
|
|
12294
|
+
decision: input.decision,
|
|
12295
|
+
approver_set: input.approver_set,
|
|
12296
|
+
granted_by_user_id: input.granted_by_user_id,
|
|
12297
|
+
ttl_ms: input.ttl_ms
|
|
12298
|
+
}, now);
|
|
12299
|
+
return { consumed: true, decision_id, nonce };
|
|
12300
|
+
});
|
|
12301
|
+
return run();
|
|
12302
|
+
}
|
|
12247
12303
|
function revokeDecision(db, decision_id, actor, reason, now = Date.now()) {
|
|
12248
12304
|
const row = db.query(`SELECT * FROM approval_decisions WHERE id = ?`).get(decision_id);
|
|
12249
12305
|
if (!row)
|
|
@@ -12953,6 +13009,37 @@ function handleRequest(socket, req, agent, db, peerUid, isOperator = false) {
|
|
|
12953
13009
|
socket.write(encodeResponse({ ok: true, decision_id }));
|
|
12954
13010
|
return;
|
|
12955
13011
|
}
|
|
13012
|
+
if (req.op === "approval_consume_record") {
|
|
13013
|
+
const peek = getNonce(db, req.request_id);
|
|
13014
|
+
if (peek !== null) {
|
|
13015
|
+
const acl = checkApprovalAclByAgent(agent, peek.agent_unit);
|
|
13016
|
+
if (!acl.allow) {
|
|
13017
|
+
socket.write(encodeResponse(errorResponse("DENIED", "approval_consume_record denied: nonce does not belong to the calling agent")));
|
|
13018
|
+
return;
|
|
13019
|
+
}
|
|
13020
|
+
}
|
|
13021
|
+
const res = consumeAndRecord(db, {
|
|
13022
|
+
request_id: req.request_id,
|
|
13023
|
+
decision: req.decision,
|
|
13024
|
+
approver_set: req.approver_set,
|
|
13025
|
+
granted_by_user_id: req.granted_by_user_id,
|
|
13026
|
+
ttl_ms: req.ttl_ms ?? undefined
|
|
13027
|
+
});
|
|
13028
|
+
if (!res.consumed) {
|
|
13029
|
+
socket.write(encodeResponse({ ok: true, consumed: false }));
|
|
13030
|
+
return;
|
|
13031
|
+
}
|
|
13032
|
+
socket.write(encodeResponse({
|
|
13033
|
+
ok: true,
|
|
13034
|
+
consumed: true,
|
|
13035
|
+
decision_id: res.decision_id,
|
|
13036
|
+
agent_unit: res.nonce.agent_unit,
|
|
13037
|
+
scope: res.nonce.scope,
|
|
13038
|
+
action: res.nonce.action,
|
|
13039
|
+
why: res.nonce.why
|
|
13040
|
+
}));
|
|
13041
|
+
return;
|
|
13042
|
+
}
|
|
12956
13043
|
if (req.op === "approval_list") {
|
|
12957
13044
|
const decisions = listDecisions(db, { agent_unit: req.agent_unit });
|
|
12958
13045
|
const meta = decisions.map((d) => ({
|
|
@@ -12899,6 +12899,48 @@ function readAutoUnlockFile(filePath) {
|
|
|
12899
12899
|
}
|
|
12900
12900
|
var DEFAULT_AUTO_UNLOCK_PATH = "~/.switchroom/vault-auto-unlock";
|
|
12901
12901
|
|
|
12902
|
+
// src/config/google-workspace-acl.ts
|
|
12903
|
+
function shouldEmitGdriveMcp(agentName, agentGoogleAccount, googleAccounts) {
|
|
12904
|
+
if (!agentGoogleAccount)
|
|
12905
|
+
return false;
|
|
12906
|
+
const account = agentGoogleAccount.trim().toLowerCase();
|
|
12907
|
+
if (account.length === 0)
|
|
12908
|
+
return false;
|
|
12909
|
+
const acctEntry = googleAccounts?.[account];
|
|
12910
|
+
if (!acctEntry)
|
|
12911
|
+
return false;
|
|
12912
|
+
const enabledFor = acctEntry.enabled_for ?? [];
|
|
12913
|
+
return enabledFor.includes(agentName);
|
|
12914
|
+
}
|
|
12915
|
+
function vaultRefKey(value) {
|
|
12916
|
+
if (typeof value !== "string" || !value.startsWith("vault:"))
|
|
12917
|
+
return null;
|
|
12918
|
+
const key = value.slice("vault:".length).split("#")[0];
|
|
12919
|
+
return key.length > 0 ? key : null;
|
|
12920
|
+
}
|
|
12921
|
+
function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
|
|
12922
|
+
if (!agentName || !key)
|
|
12923
|
+
return false;
|
|
12924
|
+
const agentConfig = config.agents?.[agentName];
|
|
12925
|
+
if (!agentConfig)
|
|
12926
|
+
return false;
|
|
12927
|
+
if (agentConfig.mcp_servers?.["gdrive"] === false) {
|
|
12928
|
+
return false;
|
|
12929
|
+
}
|
|
12930
|
+
const account = agentConfig.google_workspace?.account;
|
|
12931
|
+
if (!shouldEmitGdriveMcp(agentName, account, config.google_accounts)) {
|
|
12932
|
+
return false;
|
|
12933
|
+
}
|
|
12934
|
+
const gw = config.google_workspace;
|
|
12935
|
+
if (!gw)
|
|
12936
|
+
return false;
|
|
12937
|
+
for (const ref of [gw.google_client_id, gw.google_client_secret]) {
|
|
12938
|
+
if (vaultRefKey(ref) === key)
|
|
12939
|
+
return true;
|
|
12940
|
+
}
|
|
12941
|
+
return false;
|
|
12942
|
+
}
|
|
12943
|
+
|
|
12902
12944
|
// src/vault/broker/acl.ts
|
|
12903
12945
|
function parseCronUnit(unitName) {
|
|
12904
12946
|
const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
|
|
@@ -12986,6 +13028,9 @@ function checkAclByAgent(config, agentName, key) {
|
|
|
12986
13028
|
if (googleSlot !== null) {
|
|
12987
13029
|
return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
|
|
12988
13030
|
}
|
|
13031
|
+
if (isGoogleClientCredentialKeyForAgent(config, agentName, key)) {
|
|
13032
|
+
return { allow: true };
|
|
13033
|
+
}
|
|
12989
13034
|
const agentBot = agentConfig.bot_token;
|
|
12990
13035
|
const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
|
|
12991
13036
|
if (typeof botRef === "string" && botRef.startsWith("vault:")) {
|
|
@@ -13102,6 +13147,24 @@ var LockRequestSchema = exports_external.object({
|
|
|
13102
13147
|
v: exports_external.literal(1),
|
|
13103
13148
|
op: exports_external.literal("lock")
|
|
13104
13149
|
});
|
|
13150
|
+
var PreflightAccessRequestSchema = exports_external.object({
|
|
13151
|
+
v: exports_external.literal(1),
|
|
13152
|
+
op: exports_external.literal("preflight_access"),
|
|
13153
|
+
agent: exports_external.string().min(1),
|
|
13154
|
+
keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
|
|
13155
|
+
});
|
|
13156
|
+
var OkPreflightAccessResponseSchema = exports_external.object({
|
|
13157
|
+
ok: exports_external.literal(true),
|
|
13158
|
+
op: exports_external.literal("preflight_access"),
|
|
13159
|
+
results: exports_external.array(exports_external.object({
|
|
13160
|
+
key: exports_external.string(),
|
|
13161
|
+
exists: exports_external.boolean(),
|
|
13162
|
+
acl_ok: exports_external.boolean(),
|
|
13163
|
+
acl_reason: exports_external.string().optional(),
|
|
13164
|
+
scope_ok: exports_external.boolean(),
|
|
13165
|
+
scope_reason: exports_external.string().optional()
|
|
13166
|
+
}))
|
|
13167
|
+
});
|
|
13105
13168
|
var ApprovalRequestRequestSchema = exports_external.object({
|
|
13106
13169
|
v: exports_external.literal(1),
|
|
13107
13170
|
op: exports_external.literal("approval_request"),
|
|
@@ -13153,12 +13216,22 @@ var ApprovalRecordRequestSchema = exports_external.object({
|
|
|
13153
13216
|
granted_by_user_id: exports_external.number().int(),
|
|
13154
13217
|
ttl_ms: exports_external.number().int().positive().nullable().optional()
|
|
13155
13218
|
});
|
|
13219
|
+
var ApprovalConsumeRecordRequestSchema = exports_external.object({
|
|
13220
|
+
v: exports_external.literal(1),
|
|
13221
|
+
op: exports_external.literal("approval_consume_record"),
|
|
13222
|
+
request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
|
|
13223
|
+
decision: ApprovalDecisionModeSchema,
|
|
13224
|
+
approver_set: exports_external.array(exports_external.string()),
|
|
13225
|
+
granted_by_user_id: exports_external.number().int(),
|
|
13226
|
+
ttl_ms: exports_external.number().int().positive().nullable().optional()
|
|
13227
|
+
});
|
|
13156
13228
|
var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
13157
13229
|
GetRequestSchema,
|
|
13158
13230
|
PutRequestSchema,
|
|
13159
13231
|
ListRequestSchema,
|
|
13160
13232
|
StatusRequestSchema,
|
|
13161
13233
|
LockRequestSchema,
|
|
13234
|
+
PreflightAccessRequestSchema,
|
|
13162
13235
|
MintGrantRequestSchema,
|
|
13163
13236
|
ListGrantsRequestSchema,
|
|
13164
13237
|
RevokeGrantRequestSchema,
|
|
@@ -13167,7 +13240,8 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
|
13167
13240
|
ApprovalConsumeRequestSchema,
|
|
13168
13241
|
ApprovalRevokeRequestSchema,
|
|
13169
13242
|
ApprovalListRequestSchema,
|
|
13170
|
-
ApprovalRecordRequestSchema
|
|
13243
|
+
ApprovalRecordRequestSchema,
|
|
13244
|
+
ApprovalConsumeRecordRequestSchema
|
|
13171
13245
|
]);
|
|
13172
13246
|
var VaultEntrySchema = exports_external.union([
|
|
13173
13247
|
exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
|
|
@@ -13289,6 +13363,15 @@ var OkApprovalRecordResponseSchema = exports_external.object({
|
|
|
13289
13363
|
ok: exports_external.literal(true),
|
|
13290
13364
|
decision_id: exports_external.string()
|
|
13291
13365
|
});
|
|
13366
|
+
var OkApprovalConsumeRecordResponseSchema = exports_external.object({
|
|
13367
|
+
ok: exports_external.literal(true),
|
|
13368
|
+
consumed: exports_external.boolean(),
|
|
13369
|
+
decision_id: exports_external.string().optional(),
|
|
13370
|
+
agent_unit: exports_external.string().optional(),
|
|
13371
|
+
scope: exports_external.string().optional(),
|
|
13372
|
+
action: exports_external.string().optional(),
|
|
13373
|
+
why: exports_external.string().nullable().optional()
|
|
13374
|
+
});
|
|
13292
13375
|
var ErrorResponseSchema = exports_external.object({
|
|
13293
13376
|
ok: exports_external.literal(false),
|
|
13294
13377
|
code: ErrorCode,
|
|
@@ -13299,6 +13382,7 @@ var ResponseSchema = exports_external.union([
|
|
|
13299
13382
|
OkKeysResponseSchema,
|
|
13300
13383
|
OkStatusResponseSchema,
|
|
13301
13384
|
OkLockResponseSchema,
|
|
13385
|
+
OkPreflightAccessResponseSchema,
|
|
13302
13386
|
OkPutResponseSchema,
|
|
13303
13387
|
OkMintGrantResponseSchema,
|
|
13304
13388
|
OkListGrantsResponseSchema,
|
|
@@ -13309,6 +13393,7 @@ var ResponseSchema = exports_external.union([
|
|
|
13309
13393
|
OkApprovalRevokeResponseSchema,
|
|
13310
13394
|
OkApprovalListResponseSchema,
|
|
13311
13395
|
OkApprovalRecordResponseSchema,
|
|
13396
|
+
OkApprovalConsumeRecordResponseSchema,
|
|
13312
13397
|
ErrorResponseSchema
|
|
13313
13398
|
]);
|
|
13314
13399
|
function decodeRequest(line) {
|
|
@@ -15741,7 +15826,9 @@ class VaultBroker {
|
|
|
15741
15826
|
this._writePidFile();
|
|
15742
15827
|
this._sdNotify(`READY=1
|
|
15743
15828
|
`);
|
|
15744
|
-
this.
|
|
15829
|
+
if (this.testOpts._testSecrets === undefined) {
|
|
15830
|
+
this._tryAutoUnlock();
|
|
15831
|
+
}
|
|
15745
15832
|
if (process.platform !== "linux") {
|
|
15746
15833
|
process.stderr.write(`[vault-broker] WARNING: running on ${process.platform} with ` + `SWITCHROOM_BROKER_ALLOW_NON_LINUX=1 — peercred ACL is disabled. ` + `Access control is socket file mode 0600 ONLY. Do not use this configuration for production secrets.
|
|
15747
15834
|
`);
|
|
@@ -16052,6 +16139,49 @@ class VaultBroker {
|
|
|
16052
16139
|
socket.write(encodeResponse({ ok: true, locked: true }));
|
|
16053
16140
|
return;
|
|
16054
16141
|
}
|
|
16142
|
+
if (req.op === "preflight_access") {
|
|
16143
|
+
if (!isOperator) {
|
|
16144
|
+
writeAudit({
|
|
16145
|
+
ts: new Date().toISOString(),
|
|
16146
|
+
op: "preflight_access",
|
|
16147
|
+
caller: auditCaller,
|
|
16148
|
+
pid: auditPid,
|
|
16149
|
+
cgroup: auditCgroup,
|
|
16150
|
+
result: "denied:operator-only"
|
|
16151
|
+
});
|
|
16152
|
+
socket.write(encodeResponse(errorResponse("DENIED", "preflight_access is operator-only")));
|
|
16153
|
+
return;
|
|
16154
|
+
}
|
|
16155
|
+
if (this.secrets === null) {
|
|
16156
|
+
socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
|
|
16157
|
+
return;
|
|
16158
|
+
}
|
|
16159
|
+
const pfSecrets = this.secrets;
|
|
16160
|
+
const pfConfig = this.config;
|
|
16161
|
+
const results = req.keys.map((key) => {
|
|
16162
|
+
const exists = Object.prototype.hasOwnProperty.call(pfSecrets, key);
|
|
16163
|
+
const acl = pfConfig !== null ? checkAclByAgent(pfConfig, req.agent, key) : { allow: false, reason: "broker has no config loaded" };
|
|
16164
|
+
const scope = checkEntryScope(pfSecrets[key]?.scope, req.agent);
|
|
16165
|
+
return {
|
|
16166
|
+
key,
|
|
16167
|
+
exists,
|
|
16168
|
+
acl_ok: acl.allow,
|
|
16169
|
+
...acl.allow ? {} : { acl_reason: acl.reason },
|
|
16170
|
+
scope_ok: scope.allow,
|
|
16171
|
+
...scope.allow ? {} : { scope_reason: scope.reason }
|
|
16172
|
+
};
|
|
16173
|
+
});
|
|
16174
|
+
writeAudit({
|
|
16175
|
+
ts: new Date().toISOString(),
|
|
16176
|
+
op: "preflight_access",
|
|
16177
|
+
caller: auditCaller,
|
|
16178
|
+
pid: auditPid,
|
|
16179
|
+
cgroup: auditCgroup,
|
|
16180
|
+
result: `allowed:agent=${req.agent},keys=${req.keys.length}`
|
|
16181
|
+
});
|
|
16182
|
+
socket.write(encodeResponse({ ok: true, op: "preflight_access", results }));
|
|
16183
|
+
return;
|
|
16184
|
+
}
|
|
16055
16185
|
if (req.op === "list") {
|
|
16056
16186
|
if (this.secrets === null) {
|
|
16057
16187
|
socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
|
package/package.json
CHANGED