switchroom 0.14.35 → 0.14.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/agent-scheduler/index.js +2 -1
- package/dist/auth-broker/index.js +2 -1
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +53 -3
- package/dist/host-control/main.js +2 -1
- package/dist/vault/approvals/kernel-server.js +2 -1
- package/dist/vault/broker/server.js +51 -1
- package/package.json +2 -2
- package/telegram-plugin/dist/gateway/gateway.js +38 -7
- package/telegram-plugin/gateway/gateway.ts +41 -1
|
@@ -11386,7 +11386,8 @@ var VaultConfigSchema = exports_external.object({
|
|
|
11386
11386
|
autoUnlock: exports_external.boolean().default(false).describe("Auto-unlock the vault at broker start using a machine-bound " + "encrypted blob. Off by default. When enabled, the broker reads " + "the configured blob path, derives the AES key from /etc/machine-id, " + "decrypts the passphrase, and unlocks the vault — no sudo, no " + "systemd-creds, no TPM. Run `switchroom vault broker " + "enable-auto-unlock` once to write the blob."),
|
|
11387
11387
|
autoUnlockCredentialPath: exports_external.string().default("~/.switchroom/vault-auto-unlock").describe("Path to the machine-bound auto-unlock blob (see " + "src/vault/auto-unlock.ts for the format). Default lives under " + "~/.switchroom so it can be bind-mounted into the vault-broker " + "container by docker compose. Tilde-expansion happens " + "at read time."),
|
|
11388
11388
|
approvalAuth: exports_external.enum(["passphrase", "telegram-id"]).default("passphrase").describe("Posture for tap-to-Approve on vault grant cards. `passphrase` " + "(default) prompts the operator to type the vault passphrase on " + "every Approve — two-factor (Telegram ID + passphrase). " + "`telegram-id` mints immediately on Approve with no passphrase " + "prompt — single-factor (Telegram ID only); REQUIRES " + "`autoUnlock: true` so the broker already holds the passphrase. " + "Trades a factor of security for smoother UX; opt-in only."),
|
|
11389
|
-
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config.")
|
|
11389
|
+
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config."),
|
|
11390
|
+
adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation — granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` — no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload).")
|
|
11390
11391
|
}).default({}).superRefine((broker, ctx) => {
|
|
11391
11392
|
if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
|
|
11392
11393
|
ctx.addIssue({
|
|
@@ -11386,7 +11386,8 @@ var VaultConfigSchema = exports_external.object({
|
|
|
11386
11386
|
autoUnlock: exports_external.boolean().default(false).describe("Auto-unlock the vault at broker start using a machine-bound " + "encrypted blob. Off by default. When enabled, the broker reads " + "the configured blob path, derives the AES key from /etc/machine-id, " + "decrypts the passphrase, and unlocks the vault — no sudo, no " + "systemd-creds, no TPM. Run `switchroom vault broker " + "enable-auto-unlock` once to write the blob."),
|
|
11387
11387
|
autoUnlockCredentialPath: exports_external.string().default("~/.switchroom/vault-auto-unlock").describe("Path to the machine-bound auto-unlock blob (see " + "src/vault/auto-unlock.ts for the format). Default lives under " + "~/.switchroom so it can be bind-mounted into the vault-broker " + "container by docker compose. Tilde-expansion happens " + "at read time."),
|
|
11388
11388
|
approvalAuth: exports_external.enum(["passphrase", "telegram-id"]).default("passphrase").describe("Posture for tap-to-Approve on vault grant cards. `passphrase` " + "(default) prompts the operator to type the vault passphrase on " + "every Approve — two-factor (Telegram ID + passphrase). " + "`telegram-id` mints immediately on Approve with no passphrase " + "prompt — single-factor (Telegram ID only); REQUIRES " + "`autoUnlock: true` so the broker already holds the passphrase. " + "Trades a factor of security for smoother UX; opt-in only."),
|
|
11389
|
-
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config.")
|
|
11389
|
+
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config."),
|
|
11390
|
+
adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation — granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` — no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload).")
|
|
11390
11391
|
}).default({}).superRefine((broker, ctx) => {
|
|
11391
11392
|
if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
|
|
11392
11393
|
ctx.addIssue({
|
|
@@ -12134,7 +12134,8 @@ var VaultConfigSchema = exports_external.object({
|
|
|
12134
12134
|
autoUnlock: exports_external.boolean().default(false).describe("Auto-unlock the vault at broker start using a machine-bound " + "encrypted blob. Off by default. When enabled, the broker reads " + "the configured blob path, derives the AES key from /etc/machine-id, " + "decrypts the passphrase, and unlocks the vault \u2014 no sudo, no " + "systemd-creds, no TPM. Run `switchroom vault broker " + "enable-auto-unlock` once to write the blob."),
|
|
12135
12135
|
autoUnlockCredentialPath: exports_external.string().default("~/.switchroom/vault-auto-unlock").describe("Path to the machine-bound auto-unlock blob (see " + "src/vault/auto-unlock.ts for the format). Default lives under " + "~/.switchroom so it can be bind-mounted into the vault-broker " + "container by docker compose. Tilde-expansion happens " + "at read time."),
|
|
12136
12136
|
approvalAuth: exports_external.enum(["passphrase", "telegram-id"]).default("passphrase").describe("Posture for tap-to-Approve on vault grant cards. `passphrase` " + "(default) prompts the operator to type the vault passphrase on " + "every Approve \u2014 two-factor (Telegram ID + passphrase). " + "`telegram-id` mints immediately on Approve with no passphrase " + "prompt \u2014 single-factor (Telegram ID only); REQUIRES " + "`autoUnlock: true` so the broker already holds the passphrase. " + "Trades a factor of security for smoother UX; opt-in only."),
|
|
12137
|
-
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` \u2014 no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored \u2014 passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config.")
|
|
12137
|
+
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` \u2014 no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored \u2014 passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config."),
|
|
12138
|
+
adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation \u2014 granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` \u2014 no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload).")
|
|
12138
12139
|
}).default({}).superRefine((broker, ctx) => {
|
|
12139
12140
|
if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
|
|
12140
12141
|
ctx.addIssue({
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -13950,7 +13950,8 @@ var init_schema = __esm(() => {
|
|
|
13950
13950
|
autoUnlock: exports_external.boolean().default(false).describe("Auto-unlock the vault at broker start using a machine-bound " + "encrypted blob. Off by default. When enabled, the broker reads " + "the configured blob path, derives the AES key from /etc/machine-id, " + "decrypts the passphrase, and unlocks the vault \u2014 no sudo, no " + "systemd-creds, no TPM. Run `switchroom vault broker " + "enable-auto-unlock` once to write the blob."),
|
|
13951
13951
|
autoUnlockCredentialPath: exports_external.string().default("~/.switchroom/vault-auto-unlock").describe("Path to the machine-bound auto-unlock blob (see " + "src/vault/auto-unlock.ts for the format). Default lives under " + "~/.switchroom so it can be bind-mounted into the vault-broker " + "container by docker compose. Tilde-expansion happens " + "at read time."),
|
|
13952
13952
|
approvalAuth: exports_external.enum(["passphrase", "telegram-id"]).default("passphrase").describe("Posture for tap-to-Approve on vault grant cards. `passphrase` " + "(default) prompts the operator to type the vault passphrase on " + "every Approve \u2014 two-factor (Telegram ID + passphrase). " + "`telegram-id` mints immediately on Approve with no passphrase " + "prompt \u2014 single-factor (Telegram ID only); REQUIRES " + "`autoUnlock: true` so the broker already holds the passphrase. " + "Trades a factor of security for smoother UX; opt-in only."),
|
|
13953
|
-
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` \u2014 no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored \u2014 passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config.")
|
|
13953
|
+
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` \u2014 no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored \u2014 passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config."),
|
|
13954
|
+
adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation \u2014 granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` \u2014 no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload).")
|
|
13954
13955
|
}).default({}).superRefine((broker, ctx) => {
|
|
13955
13956
|
if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
|
|
13956
13957
|
ctx.addIssue({
|
|
@@ -49439,8 +49440,8 @@ var {
|
|
|
49439
49440
|
} = import__.default;
|
|
49440
49441
|
|
|
49441
49442
|
// src/build-info.ts
|
|
49442
|
-
var VERSION = "0.14.
|
|
49443
|
-
var COMMIT_SHA = "
|
|
49443
|
+
var VERSION = "0.14.36";
|
|
49444
|
+
var COMMIT_SHA = "127b6f28";
|
|
49444
49445
|
|
|
49445
49446
|
// src/cli/agent.ts
|
|
49446
49447
|
init_source();
|
|
@@ -61321,6 +61322,31 @@ function listGrants(db, agent_slug) {
|
|
|
61321
61322
|
return rows.map(rowToGrant);
|
|
61322
61323
|
}
|
|
61323
61324
|
|
|
61325
|
+
// src/vault/admin-only-keys.ts
|
|
61326
|
+
function globToRegExp(pattern) {
|
|
61327
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
61328
|
+
return new RegExp(`^${escaped}$`);
|
|
61329
|
+
}
|
|
61330
|
+
function matchesAdminOnlyKey(key, patterns) {
|
|
61331
|
+
for (const p of patterns) {
|
|
61332
|
+
if (p === key)
|
|
61333
|
+
return true;
|
|
61334
|
+
if (p.includes("*") && globToRegExp(p).test(key))
|
|
61335
|
+
return true;
|
|
61336
|
+
}
|
|
61337
|
+
return false;
|
|
61338
|
+
}
|
|
61339
|
+
function adminOnlyKeysBeingAdded(requestedKeys, existingKeys, patterns) {
|
|
61340
|
+
const existing = new Set(existingKeys);
|
|
61341
|
+
const blocked = [];
|
|
61342
|
+
for (const k of requestedKeys) {
|
|
61343
|
+
if (matchesAdminOnlyKey(k, patterns) && !existing.has(k) && !blocked.includes(k)) {
|
|
61344
|
+
blocked.push(k);
|
|
61345
|
+
}
|
|
61346
|
+
}
|
|
61347
|
+
return blocked;
|
|
61348
|
+
}
|
|
61349
|
+
|
|
61324
61350
|
// src/vault/grants-db.ts
|
|
61325
61351
|
import * as os3 from "node:os";
|
|
61326
61352
|
import * as path2 from "node:path";
|
|
@@ -62699,6 +62725,30 @@ class VaultBroker {
|
|
|
62699
62725
|
socket.write(encodeResponse(errorResponse("DENIED", `posture-attested list refused: request.agent=${reqAgent} but calling peer is ${agentName}`)));
|
|
62700
62726
|
return;
|
|
62701
62727
|
}
|
|
62728
|
+
if (req.op === "mint_grant") {
|
|
62729
|
+
const adminOnly = this.config?.vault?.broker?.adminOnlyKeys ?? [];
|
|
62730
|
+
if (adminOnly.length > 0) {
|
|
62731
|
+
const requested = [
|
|
62732
|
+
...req.keys ?? [],
|
|
62733
|
+
...req.write_keys ?? []
|
|
62734
|
+
];
|
|
62735
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
62736
|
+
const existingKeys = listGrants(this.grantsDb, agentName).filter((g) => g.expires_at === null || g.expires_at > nowSec).flatMap((g) => [...g.key_allow ?? [], ...g.write_allow ?? []]);
|
|
62737
|
+
const blocked = adminOnlyKeysBeingAdded(requested, existingKeys, adminOnly);
|
|
62738
|
+
if (blocked.length > 0) {
|
|
62739
|
+
writeAudit({
|
|
62740
|
+
ts: new Date().toISOString(),
|
|
62741
|
+
op: req.op,
|
|
62742
|
+
caller: auditCaller,
|
|
62743
|
+
pid: auditPid,
|
|
62744
|
+
cgroup: auditCgroup,
|
|
62745
|
+
result: "denied:posture-mint-admin-only-key"
|
|
62746
|
+
});
|
|
62747
|
+
socket.write(encodeResponse(errorResponse("DENIED", `admin-only credential(s) ${blocked.join(", ")} cannot be minted via posture ` + `attestation \u2014 they require the operator passphrase (vault.broker.adminOnlyKeys)`)));
|
|
62748
|
+
return;
|
|
62749
|
+
}
|
|
62750
|
+
}
|
|
62751
|
+
}
|
|
62702
62752
|
if (this.passphrase === null) {
|
|
62703
62753
|
writeAudit({
|
|
62704
62754
|
ts: new Date().toISOString(),
|
|
@@ -14121,7 +14121,8 @@ var VaultConfigSchema = exports_external.object({
|
|
|
14121
14121
|
autoUnlock: exports_external.boolean().default(false).describe("Auto-unlock the vault at broker start using a machine-bound " + "encrypted blob. Off by default. When enabled, the broker reads " + "the configured blob path, derives the AES key from /etc/machine-id, " + "decrypts the passphrase, and unlocks the vault — no sudo, no " + "systemd-creds, no TPM. Run `switchroom vault broker " + "enable-auto-unlock` once to write the blob."),
|
|
14122
14122
|
autoUnlockCredentialPath: exports_external.string().default("~/.switchroom/vault-auto-unlock").describe("Path to the machine-bound auto-unlock blob (see " + "src/vault/auto-unlock.ts for the format). Default lives under " + "~/.switchroom so it can be bind-mounted into the vault-broker " + "container by docker compose. Tilde-expansion happens " + "at read time."),
|
|
14123
14123
|
approvalAuth: exports_external.enum(["passphrase", "telegram-id"]).default("passphrase").describe("Posture for tap-to-Approve on vault grant cards. `passphrase` " + "(default) prompts the operator to type the vault passphrase on " + "every Approve — two-factor (Telegram ID + passphrase). " + "`telegram-id` mints immediately on Approve with no passphrase " + "prompt — single-factor (Telegram ID only); REQUIRES " + "`autoUnlock: true` so the broker already holds the passphrase. " + "Trades a factor of security for smoother UX; opt-in only."),
|
|
14124
|
-
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config.")
|
|
14124
|
+
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config."),
|
|
14125
|
+
adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation — granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` — no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload).")
|
|
14125
14126
|
}).default({}).superRefine((broker, ctx) => {
|
|
14126
14127
|
if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
|
|
14127
14128
|
ctx.addIssue({
|
|
@@ -11707,7 +11707,8 @@ var init_schema = __esm(() => {
|
|
|
11707
11707
|
autoUnlock: exports_external.boolean().default(false).describe("Auto-unlock the vault at broker start using a machine-bound " + "encrypted blob. Off by default. When enabled, the broker reads " + "the configured blob path, derives the AES key from /etc/machine-id, " + "decrypts the passphrase, and unlocks the vault — no sudo, no " + "systemd-creds, no TPM. Run `switchroom vault broker " + "enable-auto-unlock` once to write the blob."),
|
|
11708
11708
|
autoUnlockCredentialPath: exports_external.string().default("~/.switchroom/vault-auto-unlock").describe("Path to the machine-bound auto-unlock blob (see " + "src/vault/auto-unlock.ts for the format). Default lives under " + "~/.switchroom so it can be bind-mounted into the vault-broker " + "container by docker compose. Tilde-expansion happens " + "at read time."),
|
|
11709
11709
|
approvalAuth: exports_external.enum(["passphrase", "telegram-id"]).default("passphrase").describe("Posture for tap-to-Approve on vault grant cards. `passphrase` " + "(default) prompts the operator to type the vault passphrase on " + "every Approve — two-factor (Telegram ID + passphrase). " + "`telegram-id` mints immediately on Approve with no passphrase " + "prompt — single-factor (Telegram ID only); REQUIRES " + "`autoUnlock: true` so the broker already holds the passphrase. " + "Trades a factor of security for smoother UX; opt-in only."),
|
|
11710
|
-
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config.")
|
|
11710
|
+
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config."),
|
|
11711
|
+
adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation — granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` — no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload).")
|
|
11711
11712
|
}).default({}).superRefine((broker, ctx) => {
|
|
11712
11713
|
if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
|
|
11713
11714
|
ctx.addIssue({
|
|
@@ -11707,7 +11707,8 @@ var init_schema = __esm(() => {
|
|
|
11707
11707
|
autoUnlock: exports_external.boolean().default(false).describe("Auto-unlock the vault at broker start using a machine-bound " + "encrypted blob. Off by default. When enabled, the broker reads " + "the configured blob path, derives the AES key from /etc/machine-id, " + "decrypts the passphrase, and unlocks the vault — no sudo, no " + "systemd-creds, no TPM. Run `switchroom vault broker " + "enable-auto-unlock` once to write the blob."),
|
|
11708
11708
|
autoUnlockCredentialPath: exports_external.string().default("~/.switchroom/vault-auto-unlock").describe("Path to the machine-bound auto-unlock blob (see " + "src/vault/auto-unlock.ts for the format). Default lives under " + "~/.switchroom so it can be bind-mounted into the vault-broker " + "container by docker compose. Tilde-expansion happens " + "at read time."),
|
|
11709
11709
|
approvalAuth: exports_external.enum(["passphrase", "telegram-id"]).default("passphrase").describe("Posture for tap-to-Approve on vault grant cards. `passphrase` " + "(default) prompts the operator to type the vault passphrase on " + "every Approve — two-factor (Telegram ID + passphrase). " + "`telegram-id` mints immediately on Approve with no passphrase " + "prompt — single-factor (Telegram ID only); REQUIRES " + "`autoUnlock: true` so the broker already holds the passphrase. " + "Trades a factor of security for smoother UX; opt-in only."),
|
|
11710
|
-
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config.")
|
|
11710
|
+
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config."),
|
|
11711
|
+
adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation — granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` — no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload).")
|
|
11711
11712
|
}).default({}).superRefine((broker, ctx) => {
|
|
11712
11713
|
if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
|
|
11713
11714
|
ctx.addIssue({
|
|
@@ -15712,6 +15713,31 @@ function listGrants(db, agent_slug) {
|
|
|
15712
15713
|
return rows.map(rowToGrant);
|
|
15713
15714
|
}
|
|
15714
15715
|
|
|
15716
|
+
// src/vault/admin-only-keys.ts
|
|
15717
|
+
function globToRegExp(pattern) {
|
|
15718
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
15719
|
+
return new RegExp(`^${escaped}$`);
|
|
15720
|
+
}
|
|
15721
|
+
function matchesAdminOnlyKey(key, patterns) {
|
|
15722
|
+
for (const p of patterns) {
|
|
15723
|
+
if (p === key)
|
|
15724
|
+
return true;
|
|
15725
|
+
if (p.includes("*") && globToRegExp(p).test(key))
|
|
15726
|
+
return true;
|
|
15727
|
+
}
|
|
15728
|
+
return false;
|
|
15729
|
+
}
|
|
15730
|
+
function adminOnlyKeysBeingAdded(requestedKeys, existingKeys, patterns) {
|
|
15731
|
+
const existing = new Set(existingKeys);
|
|
15732
|
+
const blocked = [];
|
|
15733
|
+
for (const k of requestedKeys) {
|
|
15734
|
+
if (matchesAdminOnlyKey(k, patterns) && !existing.has(k) && !blocked.includes(k)) {
|
|
15735
|
+
blocked.push(k);
|
|
15736
|
+
}
|
|
15737
|
+
}
|
|
15738
|
+
return blocked;
|
|
15739
|
+
}
|
|
15740
|
+
|
|
15715
15741
|
// src/vault/grants-db.ts
|
|
15716
15742
|
import * as os2 from "node:os";
|
|
15717
15743
|
import * as path2 from "node:path";
|
|
@@ -17090,6 +17116,30 @@ class VaultBroker {
|
|
|
17090
17116
|
socket.write(encodeResponse(errorResponse("DENIED", `posture-attested list refused: request.agent=${reqAgent} but calling peer is ${agentName}`)));
|
|
17091
17117
|
return;
|
|
17092
17118
|
}
|
|
17119
|
+
if (req.op === "mint_grant") {
|
|
17120
|
+
const adminOnly = this.config?.vault?.broker?.adminOnlyKeys ?? [];
|
|
17121
|
+
if (adminOnly.length > 0) {
|
|
17122
|
+
const requested = [
|
|
17123
|
+
...req.keys ?? [],
|
|
17124
|
+
...req.write_keys ?? []
|
|
17125
|
+
];
|
|
17126
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
17127
|
+
const existingKeys = listGrants(this.grantsDb, agentName).filter((g) => g.expires_at === null || g.expires_at > nowSec).flatMap((g) => [...g.key_allow ?? [], ...g.write_allow ?? []]);
|
|
17128
|
+
const blocked = adminOnlyKeysBeingAdded(requested, existingKeys, adminOnly);
|
|
17129
|
+
if (blocked.length > 0) {
|
|
17130
|
+
writeAudit({
|
|
17131
|
+
ts: new Date().toISOString(),
|
|
17132
|
+
op: req.op,
|
|
17133
|
+
caller: auditCaller,
|
|
17134
|
+
pid: auditPid,
|
|
17135
|
+
cgroup: auditCgroup,
|
|
17136
|
+
result: "denied:posture-mint-admin-only-key"
|
|
17137
|
+
});
|
|
17138
|
+
socket.write(encodeResponse(errorResponse("DENIED", `admin-only credential(s) ${blocked.join(", ")} cannot be minted via posture ` + `attestation — they require the operator passphrase (vault.broker.adminOnlyKeys)`)));
|
|
17139
|
+
return;
|
|
17140
|
+
}
|
|
17141
|
+
}
|
|
17142
|
+
}
|
|
17093
17143
|
if (this.passphrase === null) {
|
|
17094
17144
|
writeAudit({
|
|
17095
17145
|
ts: new Date().toISOString(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.36",
|
|
4
4
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"pretest": "npm run build",
|
|
26
26
|
"test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
|
|
27
27
|
"test:vitest": "vitest run",
|
|
28
|
-
"test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
|
|
28
|
+
"test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
|
|
29
29
|
"test:watch": "vitest",
|
|
30
30
|
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs",
|
|
31
31
|
"lint:tsc": "tsc --noEmit",
|
|
@@ -24092,7 +24092,8 @@ var init_schema = __esm(() => {
|
|
|
24092
24092
|
autoUnlock: exports_external.boolean().default(false).describe("Auto-unlock the vault at broker start using a machine-bound " + "encrypted blob. Off by default. When enabled, the broker reads " + "the configured blob path, derives the AES key from /etc/machine-id, " + "decrypts the passphrase, and unlocks the vault \u2014 no sudo, no " + "systemd-creds, no TPM. Run `switchroom vault broker " + "enable-auto-unlock` once to write the blob."),
|
|
24093
24093
|
autoUnlockCredentialPath: exports_external.string().default("~/.switchroom/vault-auto-unlock").describe("Path to the machine-bound auto-unlock blob (see " + "src/vault/auto-unlock.ts for the format). Default lives under " + "~/.switchroom so it can be bind-mounted into the vault-broker " + "container by docker compose. Tilde-expansion happens " + "at read time."),
|
|
24094
24094
|
approvalAuth: exports_external.enum(["passphrase", "telegram-id"]).default("passphrase").describe("Posture for tap-to-Approve on vault grant cards. `passphrase` " + "(default) prompts the operator to type the vault passphrase on " + "every Approve \u2014 two-factor (Telegram ID + passphrase). " + "`telegram-id` mints immediately on Approve with no passphrase " + "prompt \u2014 single-factor (Telegram ID only); REQUIRES " + "`autoUnlock: true` so the broker already holds the passphrase. " + "Trades a factor of security for smoother UX; opt-in only."),
|
|
24095
|
-
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` \u2014 no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored \u2014 passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config.")
|
|
24095
|
+
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` \u2014 no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored \u2014 passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config."),
|
|
24096
|
+
adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation \u2014 granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` \u2014 no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload).")
|
|
24096
24097
|
}).default({}).superRefine((broker, ctx) => {
|
|
24097
24098
|
if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
|
|
24098
24099
|
ctx.addIssue({
|
|
@@ -51781,10 +51782,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
51781
51782
|
}
|
|
51782
51783
|
|
|
51783
51784
|
// ../src/build-info.ts
|
|
51784
|
-
var VERSION = "0.14.
|
|
51785
|
-
var COMMIT_SHA = "
|
|
51786
|
-
var COMMIT_DATE = "2026-06-
|
|
51787
|
-
var LATEST_PR =
|
|
51785
|
+
var VERSION = "0.14.36";
|
|
51786
|
+
var COMMIT_SHA = "127b6f28";
|
|
51787
|
+
var COMMIT_DATE = "2026-06-01T23:38:45Z";
|
|
51788
|
+
var LATEST_PR = 2074;
|
|
51788
51789
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
51789
51790
|
|
|
51790
51791
|
// gateway/boot-version.ts
|
|
@@ -52083,6 +52084,21 @@ function resolveVaultApprovalPosture(broker) {
|
|
|
52083
52084
|
return { mode: "passphrase" };
|
|
52084
52085
|
}
|
|
52085
52086
|
|
|
52087
|
+
// ../src/vault/admin-only-keys.ts
|
|
52088
|
+
function globToRegExp(pattern) {
|
|
52089
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
52090
|
+
return new RegExp(`^${escaped}$`);
|
|
52091
|
+
}
|
|
52092
|
+
function matchesAdminOnlyKey(key, patterns) {
|
|
52093
|
+
for (const p of patterns) {
|
|
52094
|
+
if (p === key)
|
|
52095
|
+
return true;
|
|
52096
|
+
if (p.includes("*") && globToRegExp(p).test(key))
|
|
52097
|
+
return true;
|
|
52098
|
+
}
|
|
52099
|
+
return false;
|
|
52100
|
+
}
|
|
52101
|
+
|
|
52086
52102
|
// registry/turns-schema.ts
|
|
52087
52103
|
import { chmodSync as chmodSync4, mkdirSync as mkdirSync22 } from "fs";
|
|
52088
52104
|
import { join as join33 } from "path";
|
|
@@ -53495,6 +53511,7 @@ function rememberAgentButtonMeta(chatId, messageId, meta) {
|
|
|
53495
53511
|
var vaultPassphraseCache = new Map;
|
|
53496
53512
|
var VAULT_PASSPHRASE_TTL_MS = 1800000;
|
|
53497
53513
|
var VAULT_APPROVAL_AUTH_MODE = "passphrase";
|
|
53514
|
+
var ADMIN_ONLY_KEYS = [];
|
|
53498
53515
|
function initVaultApprovalPosture() {
|
|
53499
53516
|
let cfg;
|
|
53500
53517
|
try {
|
|
@@ -53508,6 +53525,11 @@ function initVaultApprovalPosture() {
|
|
|
53508
53525
|
VAULT_APPROVAL_AUTH_MODE = resolved.mode;
|
|
53509
53526
|
if (resolved.mode === "telegram-id") {
|
|
53510
53527
|
process.stderr.write(`telegram gateway: vault approval posture = telegram-id ` + `(single-factor \u2014 broker mediates attestation via attest_via_posture)
|
|
53528
|
+
`);
|
|
53529
|
+
}
|
|
53530
|
+
ADMIN_ONLY_KEYS = cfg.vault?.broker?.adminOnlyKeys ?? [];
|
|
53531
|
+
if (ADMIN_ONLY_KEYS.length > 0) {
|
|
53532
|
+
process.stderr.write(`telegram gateway: ${ADMIN_ONLY_KEYS.length} admin-only vault key pattern(s) ` + `\u2014 grants approved by allowFrom[0] + operator passphrase only
|
|
53511
53533
|
`);
|
|
53512
53534
|
}
|
|
53513
53535
|
}
|
|
@@ -59724,7 +59746,14 @@ async function handleVaultRequestAccessCallback(ctx, data) {
|
|
|
59724
59746
|
return;
|
|
59725
59747
|
}
|
|
59726
59748
|
if (action === "approve") {
|
|
59727
|
-
|
|
59749
|
+
const isAdminOnly = matchesAdminOnlyKey(pending2.key, ADMIN_ONLY_KEYS);
|
|
59750
|
+
if (isAdminOnly && senderId !== access.allowFrom[0]) {
|
|
59751
|
+
await ctx.answerCallbackQuery({
|
|
59752
|
+
text: "\uD83D\uDD12 Admin-only credential \u2014 only the owner can approve this."
|
|
59753
|
+
}).catch(() => {});
|
|
59754
|
+
return;
|
|
59755
|
+
}
|
|
59756
|
+
if (!isAdminOnly && VAULT_APPROVAL_AUTH_MODE === "telegram-id") {
|
|
59728
59757
|
const username = ctx.from?.username ?? ctx.from?.first_name ?? `id=${senderId}`;
|
|
59729
59758
|
if (pending2.card_message_id != null) {
|
|
59730
59759
|
await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, `\u2705 Approved by @${escapeHtmlForTg(username)} \u2014 minting\u2026`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
@@ -59754,7 +59783,9 @@ async function handleVaultRequestAccessCallback(ctx, data) {
|
|
|
59754
59783
|
});
|
|
59755
59784
|
const joiningBatch = items.length > 1;
|
|
59756
59785
|
await ctx.answerCallbackQuery({ text: joiningBatch ? `\uD83D\uDD10 Queued \u2014 one passphrase covers ${items.length} cards` : "\uD83D\uDD10 Send your passphrase\u2026" }).catch(() => {});
|
|
59757
|
-
await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, joiningBatch ? `\uD83D\uDD10 <b>Queued behind an earlier card.</b> Type your passphrase as your next message \u2014 it covers <b>${items.length}</b> pending approvals in this chat (one entry mints all grants, no re-type per card).` : `\uD83D\
|
|
59786
|
+
await ctx.api.editMessageText(pending2.chat_id, pending2.card_message_id, joiningBatch ? `\uD83D\uDD10 <b>Queued behind an earlier card.</b> Type your passphrase as your next message \u2014 it covers <b>${items.length}</b> pending approvals in this chat (one entry mints all grants, no re-type per card).` : isAdminOnly ? `\uD83D\uDD12 <b>Admin-only credential.</b> <code>${escapeHtmlForTg(pending2.key)}</code> requires your vault passphrase to grant \u2014 reply with it as your next message and we'll mint the grant for <b>${escapeHtmlForTg(pending2.agent)}</b>, then delete the passphrase message.
|
|
59787
|
+
|
|
59788
|
+
<i>The passphrase is what proves it's you: an agent can never mint this key on its own.</i>` : `\uD83D\uDD10 <b>Vault is locked.</b> Reply with your passphrase as your next message \u2014 we'll unlock, mint the grant for <b>${escapeHtmlForTg(pending2.agent)}</b>, and delete the passphrase message in one step.
|
|
59758
59789
|
|
|
59759
59790
|
<i>Mint authority stays operator-only: the broker only accepts the grant when the passphrase matches.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
59760
59791
|
return;
|
|
@@ -420,6 +420,7 @@ import {
|
|
|
420
420
|
approvalRecord,
|
|
421
421
|
} from '../../src/vault/approvals/client.js'
|
|
422
422
|
import { resolveVaultApprovalPosture } from '../vault-approval-posture.js'
|
|
423
|
+
import { matchesAdminOnlyKey } from '../../src/vault/admin-only-keys.js'
|
|
423
424
|
import {
|
|
424
425
|
openTurnsDb,
|
|
425
426
|
markOrphanedWithTimeoutClassification,
|
|
@@ -2690,6 +2691,15 @@ const VAULT_PASSPHRASE_TTL_MS = 30 * 60 * 1000
|
|
|
2690
2691
|
*/
|
|
2691
2692
|
let VAULT_APPROVAL_AUTH_MODE: 'passphrase' | 'telegram-id' = 'passphrase'
|
|
2692
2693
|
|
|
2694
|
+
/**
|
|
2695
|
+
* Admin-only vault keys (`vault.broker.adminOnlyKeys`). A grant for one
|
|
2696
|
+
* of these may be approved ONLY by the admin operator
|
|
2697
|
+
* (`access.allowFrom[0]`) and is always minted via the operator
|
|
2698
|
+
* passphrase — never posture, even under telegram-id mode (the broker
|
|
2699
|
+
* enforces the same rule). Cached at boot alongside the posture mode.
|
|
2700
|
+
*/
|
|
2701
|
+
let ADMIN_ONLY_KEYS: string[] = []
|
|
2702
|
+
|
|
2693
2703
|
export function initVaultApprovalPosture(): void {
|
|
2694
2704
|
let cfg: ReturnType<typeof loadSwitchroomConfig>
|
|
2695
2705
|
try {
|
|
@@ -2722,6 +2732,13 @@ export function initVaultApprovalPosture(): void {
|
|
|
2722
2732
|
`(single-factor — broker mediates attestation via attest_via_posture)\n`,
|
|
2723
2733
|
)
|
|
2724
2734
|
}
|
|
2735
|
+
ADMIN_ONLY_KEYS = cfg.vault?.broker?.adminOnlyKeys ?? []
|
|
2736
|
+
if (ADMIN_ONLY_KEYS.length > 0) {
|
|
2737
|
+
process.stderr.write(
|
|
2738
|
+
`telegram gateway: ${ADMIN_ONLY_KEYS.length} admin-only vault key pattern(s) ` +
|
|
2739
|
+
`— grants approved by allowFrom[0] + operator passphrase only\n`,
|
|
2740
|
+
)
|
|
2741
|
+
}
|
|
2725
2742
|
}
|
|
2726
2743
|
/**
|
|
2727
2744
|
* Gateway-side guard on vault-key shape — UX gate, not a security
|
|
@@ -13851,11 +13868,31 @@ async function handleVaultRequestAccessCallback(ctx: Context, data: string): Pro
|
|
|
13851
13868
|
}
|
|
13852
13869
|
|
|
13853
13870
|
if (action === 'approve') {
|
|
13871
|
+
// Admin-only credentials (`vault.broker.adminOnlyKeys`) are held to a
|
|
13872
|
+
// higher bar: ONLY the admin operator (allowFrom[0]) may approve, and
|
|
13873
|
+
// the grant must be minted with the operator passphrase — never
|
|
13874
|
+
// posture, even under telegram-id mode (the broker enforces the same
|
|
13875
|
+
// rule, so a posture mint would just be rejected). So for an
|
|
13876
|
+
// admin-only key we (a) reject taps from any non-admin allowFrom
|
|
13877
|
+
// member, and (b) skip the telegram-id posture branch below, falling
|
|
13878
|
+
// through to the passphrase-prompt path. The card + buttons stay
|
|
13879
|
+
// intact on a non-admin tap so the admin can still approve.
|
|
13880
|
+
const isAdminOnly = matchesAdminOnlyKey(pending.key, ADMIN_ONLY_KEYS)
|
|
13881
|
+
if (isAdminOnly && senderId !== access.allowFrom[0]) {
|
|
13882
|
+
await ctx
|
|
13883
|
+
.answerCallbackQuery({
|
|
13884
|
+
text: '🔒 Admin-only credential — only the owner can approve this.',
|
|
13885
|
+
})
|
|
13886
|
+
.catch(() => {})
|
|
13887
|
+
return
|
|
13888
|
+
}
|
|
13889
|
+
|
|
13854
13890
|
// Posture: telegram-id (opt-in single-factor). The broker is
|
|
13855
13891
|
// auto-unlocked and we silently hold the passphrase in memory; skip
|
|
13856
13892
|
// the passphrase-cache lookup + prompt entirely and mint directly.
|
|
13857
13893
|
// Allowlist check above already attested the operator's Telegram ID.
|
|
13858
|
-
|
|
13894
|
+
// Admin-only keys are excluded — they take the passphrase path below.
|
|
13895
|
+
if (!isAdminOnly && VAULT_APPROVAL_AUTH_MODE === 'telegram-id') {
|
|
13859
13896
|
const username = ctx.from?.username ?? ctx.from?.first_name ?? `id=${senderId}`
|
|
13860
13897
|
if (pending.card_message_id != null) {
|
|
13861
13898
|
await ctx.api
|
|
@@ -13920,6 +13957,9 @@ async function handleVaultRequestAccessCallback(ctx: Context, data: string): Pro
|
|
|
13920
13957
|
pending.card_message_id,
|
|
13921
13958
|
joiningBatch
|
|
13922
13959
|
? `🔐 <b>Queued behind an earlier card.</b> Type your passphrase as your next message — it covers <b>${items.length}</b> pending approvals in this chat (one entry mints all grants, no re-type per card).`
|
|
13960
|
+
: isAdminOnly
|
|
13961
|
+
? `🔒 <b>Admin-only credential.</b> <code>${escapeHtmlForTg(pending.key)}</code> requires your vault passphrase to grant — reply with it as your next message and we'll mint the grant for <b>${escapeHtmlForTg(pending.agent)}</b>, then delete the passphrase message.\n\n` +
|
|
13962
|
+
`<i>The passphrase is what proves it's you: an agent can never mint this key on its own.</i>`
|
|
13923
13963
|
: `🔐 <b>Vault is locked.</b> Reply with your passphrase as your next message — we'll unlock, mint the grant for <b>${escapeHtmlForTg(pending.agent)}</b>, and delete the passphrase message in one step.\n\n` +
|
|
13924
13964
|
`<i>Mint authority stays operator-only: the broker only accepts the grant when the passphrase matches.</i>`,
|
|
13925
13965
|
{ parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } },
|