@poolzin/pool-bot 2026.2.23 → 2026.2.25

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.
Files changed (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -1,85 +1,28 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { resolveStateDir } from "../config/paths.js";
2
+ import { normalizeDeviceAuthScopes } from "../shared/device-auth.js";
3
+ import { roleScopesAllow } from "../shared/operator-scope-compat.js";
4
+ import { createAsyncLock, pruneExpiredPending, readJsonFile, resolvePairingPaths, upsertPendingPairingRequest, writeJsonAtomic, } from "./pairing-files.js";
5
+ import { generatePairingToken, verifyPairingToken } from "./pairing-token.js";
5
6
  const PENDING_TTL_MS = 5 * 60 * 1000;
6
- function resolvePaths(baseDir) {
7
- const root = baseDir ?? resolveStateDir();
8
- const dir = path.join(root, "devices");
9
- return {
10
- dir,
11
- pendingPath: path.join(dir, "pending.json"),
12
- pairedPath: path.join(dir, "paired.json"),
13
- };
14
- }
15
- async function readJSON(filePath) {
16
- try {
17
- const raw = await fs.readFile(filePath, "utf8");
18
- return JSON.parse(raw);
19
- }
20
- catch {
21
- return null;
22
- }
23
- }
24
- async function writeJSONAtomic(filePath, value) {
25
- const dir = path.dirname(filePath);
26
- await fs.mkdir(dir, { recursive: true });
27
- const tmp = `${filePath}.${randomUUID()}.tmp`;
28
- await fs.writeFile(tmp, JSON.stringify(value, null, 2), "utf8");
29
- try {
30
- await fs.chmod(tmp, 0o600);
31
- }
32
- catch {
33
- // best-effort
34
- }
35
- await fs.rename(tmp, filePath);
36
- try {
37
- await fs.chmod(filePath, 0o600);
38
- }
39
- catch {
40
- // best-effort
41
- }
42
- }
43
- function pruneExpiredPending(pendingById, nowMs) {
44
- for (const [id, req] of Object.entries(pendingById)) {
45
- if (nowMs - req.ts > PENDING_TTL_MS) {
46
- delete pendingById[id];
47
- }
48
- }
49
- }
50
- let lock = Promise.resolve();
51
- async function withLock(fn) {
52
- const prev = lock;
53
- let release;
54
- lock = new Promise((resolve) => {
55
- release = resolve;
56
- });
57
- await prev;
58
- try {
59
- return await fn();
60
- }
61
- finally {
62
- release?.();
63
- }
64
- }
7
+ const withLock = createAsyncLock();
65
8
  async function loadState(baseDir) {
66
- const { pendingPath, pairedPath } = resolvePaths(baseDir);
9
+ const { pendingPath, pairedPath } = resolvePairingPaths(baseDir, "devices");
67
10
  const [pending, paired] = await Promise.all([
68
- readJSON(pendingPath),
69
- readJSON(pairedPath),
11
+ readJsonFile(pendingPath),
12
+ readJsonFile(pairedPath),
70
13
  ]);
71
14
  const state = {
72
15
  pendingById: pending ?? {},
73
16
  pairedByDeviceId: paired ?? {},
74
17
  };
75
- pruneExpiredPending(state.pendingById, Date.now());
18
+ pruneExpiredPending(state.pendingById, Date.now(), PENDING_TTL_MS);
76
19
  return state;
77
20
  }
78
21
  async function persistState(state, baseDir) {
79
- const { pendingPath, pairedPath } = resolvePaths(baseDir);
22
+ const { pendingPath, pairedPath } = resolvePairingPaths(baseDir, "devices");
80
23
  await Promise.all([
81
- writeJSONAtomic(pendingPath, state.pendingById),
82
- writeJSONAtomic(pairedPath, state.pairedByDeviceId),
24
+ writeJsonAtomic(pendingPath, state.pendingById),
25
+ writeJsonAtomic(pairedPath, state.pairedByDeviceId),
83
26
  ]);
84
27
  }
85
28
  function normalizeDeviceId(deviceId) {
@@ -92,66 +35,71 @@ function normalizeRole(role) {
92
35
  function mergeRoles(...items) {
93
36
  const roles = new Set();
94
37
  for (const item of items) {
95
- if (!item)
38
+ if (!item) {
96
39
  continue;
40
+ }
97
41
  if (Array.isArray(item)) {
98
42
  for (const role of item) {
99
43
  const trimmed = role.trim();
100
- if (trimmed)
44
+ if (trimmed) {
101
45
  roles.add(trimmed);
46
+ }
102
47
  }
103
48
  }
104
49
  else {
105
50
  const trimmed = item.trim();
106
- if (trimmed)
51
+ if (trimmed) {
107
52
  roles.add(trimmed);
53
+ }
108
54
  }
109
55
  }
110
- if (roles.size === 0)
56
+ if (roles.size === 0) {
111
57
  return undefined;
58
+ }
112
59
  return [...roles];
113
60
  }
114
61
  function mergeScopes(...items) {
115
62
  const scopes = new Set();
116
63
  for (const item of items) {
117
- if (!item)
64
+ if (!item) {
118
65
  continue;
66
+ }
119
67
  for (const scope of item) {
120
68
  const trimmed = scope.trim();
121
- if (trimmed)
69
+ if (trimmed) {
122
70
  scopes.add(trimmed);
71
+ }
123
72
  }
124
73
  }
125
- if (scopes.size === 0)
74
+ if (scopes.size === 0) {
126
75
  return undefined;
76
+ }
127
77
  return [...scopes];
128
78
  }
129
- function normalizeScopes(scopes) {
130
- if (!Array.isArray(scopes))
131
- return [];
132
- const out = new Set();
133
- for (const scope of scopes) {
134
- const trimmed = scope.trim();
135
- if (trimmed)
136
- out.add(trimmed);
137
- }
138
- return [...out].sort();
79
+ function newToken() {
80
+ return generatePairingToken();
139
81
  }
140
- function scopesAllow(requested, allowed) {
141
- if (requested.length === 0)
142
- return true;
143
- if (allowed.length === 0)
144
- return false;
145
- const allowedSet = new Set(allowed);
146
- return requested.every((scope) => allowedSet.has(scope));
82
+ function getPairedDeviceFromState(state, deviceId) {
83
+ return state.pairedByDeviceId[normalizeDeviceId(deviceId)] ?? null;
147
84
  }
148
- function newToken() {
149
- return randomUUID().replaceAll("-", "");
85
+ function cloneDeviceTokens(device) {
86
+ return device.tokens ? { ...device.tokens } : {};
87
+ }
88
+ function buildDeviceAuthToken(params) {
89
+ return {
90
+ token: newToken(),
91
+ role: params.role,
92
+ scopes: params.scopes,
93
+ createdAtMs: params.existing?.createdAtMs ?? params.now,
94
+ rotatedAtMs: params.rotatedAtMs,
95
+ revokedAtMs: undefined,
96
+ lastUsedAtMs: params.existing?.lastUsedAtMs,
97
+ };
150
98
  }
151
99
  export async function listDevicePairing(baseDir) {
152
100
  const state = await loadState(baseDir);
153
- const pending = Object.values(state.pendingById).sort((a, b) => b.ts - a.ts);
154
- const paired = Object.values(state.pairedByDeviceId).sort((a, b) => b.approvedAtMs - a.approvedAtMs);
101
+ const pending = Object.values(state.pendingById).toSorted((a, b) => b.ts - a.ts);
102
+ const paired = Object.values(state.pairedByDeviceId).toSorted((a, b) => b.approvedAtMs - a.approvedAtMs);
155
103
  return { pending, paired };
156
104
  }
157
105
  export async function getPairedDevice(deviceId, baseDir) {
@@ -165,38 +113,37 @@ export async function requestDevicePairing(req, baseDir) {
165
113
  if (!deviceId) {
166
114
  throw new Error("deviceId required");
167
115
  }
168
- const existing = Object.values(state.pendingById).find((p) => p.deviceId === deviceId);
169
- if (existing) {
170
- return { status: "pending", request: existing, created: false };
171
- }
172
- const isRepair = Boolean(state.pairedByDeviceId[deviceId]);
173
- const request = {
174
- requestId: randomUUID(),
175
- deviceId,
176
- publicKey: req.publicKey,
177
- displayName: req.displayName,
178
- platform: req.platform,
179
- clientId: req.clientId,
180
- clientMode: req.clientMode,
181
- role: req.role,
182
- roles: req.role ? [req.role] : undefined,
183
- scopes: req.scopes,
184
- remoteIp: req.remoteIp,
185
- silent: req.silent,
186
- isRepair,
187
- ts: Date.now(),
188
- };
189
- state.pendingById[request.requestId] = request;
190
- await persistState(state, baseDir);
191
- return { status: "pending", request, created: true };
116
+ return await upsertPendingPairingRequest({
117
+ pendingById: state.pendingById,
118
+ isExisting: (pending) => pending.deviceId === deviceId,
119
+ isRepair: Boolean(state.pairedByDeviceId[deviceId]),
120
+ createRequest: (isRepair) => ({
121
+ requestId: randomUUID(),
122
+ deviceId,
123
+ publicKey: req.publicKey,
124
+ displayName: req.displayName,
125
+ platform: req.platform,
126
+ clientId: req.clientId,
127
+ clientMode: req.clientMode,
128
+ role: req.role,
129
+ roles: req.role ? [req.role] : undefined,
130
+ scopes: req.scopes,
131
+ remoteIp: req.remoteIp,
132
+ silent: req.silent,
133
+ isRepair,
134
+ ts: Date.now(),
135
+ }),
136
+ persist: async () => await persistState(state, baseDir),
137
+ });
192
138
  });
193
139
  }
194
140
  export async function approveDevicePairing(requestId, baseDir) {
195
141
  return await withLock(async () => {
196
142
  const state = await loadState(baseDir);
197
143
  const pending = state.pendingById[requestId];
198
- if (!pending)
144
+ if (!pending) {
199
145
  return null;
146
+ }
200
147
  const now = Date.now();
201
148
  const existing = state.pairedByDeviceId[pending.deviceId];
202
149
  const roles = mergeRoles(existing?.roles, existing?.role, pending.roles, pending.role);
@@ -204,7 +151,7 @@ export async function approveDevicePairing(requestId, baseDir) {
204
151
  const tokens = existing?.tokens ? { ...existing.tokens } : {};
205
152
  const roleForToken = normalizeRole(pending.role);
206
153
  if (roleForToken) {
207
- const nextScopes = normalizeScopes(pending.scopes);
154
+ const nextScopes = normalizeDeviceAuthScopes(pending.scopes);
208
155
  const existingToken = tokens[roleForToken];
209
156
  const now = Date.now();
210
157
  tokens[roleForToken] = {
@@ -242,19 +189,33 @@ export async function rejectDevicePairing(requestId, baseDir) {
242
189
  return await withLock(async () => {
243
190
  const state = await loadState(baseDir);
244
191
  const pending = state.pendingById[requestId];
245
- if (!pending)
192
+ if (!pending) {
246
193
  return null;
194
+ }
247
195
  delete state.pendingById[requestId];
248
196
  await persistState(state, baseDir);
249
197
  return { requestId, deviceId: pending.deviceId };
250
198
  });
251
199
  }
200
+ export async function removePairedDevice(deviceId, baseDir) {
201
+ return await withLock(async () => {
202
+ const state = await loadState(baseDir);
203
+ const normalized = normalizeDeviceId(deviceId);
204
+ if (!normalized || !state.pairedByDeviceId[normalized]) {
205
+ return null;
206
+ }
207
+ delete state.pairedByDeviceId[normalized];
208
+ await persistState(state, baseDir);
209
+ return { deviceId: normalized };
210
+ });
211
+ }
252
212
  export async function updatePairedDeviceMetadata(deviceId, patch, baseDir) {
253
213
  return await withLock(async () => {
254
214
  const state = await loadState(baseDir);
255
215
  const existing = state.pairedByDeviceId[normalizeDeviceId(deviceId)];
256
- if (!existing)
216
+ if (!existing) {
257
217
  return;
218
+ }
258
219
  const roles = mergeRoles(existing.roles, existing.role, patch.role);
259
220
  const scopes = mergeScopes(existing.scopes, patch.scopes);
260
221
  state.pairedByDeviceId[deviceId] = {
@@ -271,8 +232,9 @@ export async function updatePairedDeviceMetadata(deviceId, patch, baseDir) {
271
232
  });
272
233
  }
273
234
  export function summarizeDeviceTokens(tokens) {
274
- if (!tokens)
235
+ if (!tokens) {
275
236
  return undefined;
237
+ }
276
238
  const summaries = Object.values(tokens)
277
239
  .map((token) => ({
278
240
  role: token.role,
@@ -282,27 +244,32 @@ export function summarizeDeviceTokens(tokens) {
282
244
  revokedAtMs: token.revokedAtMs,
283
245
  lastUsedAtMs: token.lastUsedAtMs,
284
246
  }))
285
- .sort((a, b) => a.role.localeCompare(b.role));
247
+ .toSorted((a, b) => a.role.localeCompare(b.role));
286
248
  return summaries.length > 0 ? summaries : undefined;
287
249
  }
288
250
  export async function verifyDeviceToken(params) {
289
251
  return await withLock(async () => {
290
252
  const state = await loadState(params.baseDir);
291
- const device = state.pairedByDeviceId[normalizeDeviceId(params.deviceId)];
292
- if (!device)
253
+ const device = getPairedDeviceFromState(state, params.deviceId);
254
+ if (!device) {
293
255
  return { ok: false, reason: "device-not-paired" };
256
+ }
294
257
  const role = normalizeRole(params.role);
295
- if (!role)
258
+ if (!role) {
296
259
  return { ok: false, reason: "role-missing" };
260
+ }
297
261
  const entry = device.tokens?.[role];
298
- if (!entry)
262
+ if (!entry) {
299
263
  return { ok: false, reason: "token-missing" };
300
- if (entry.revokedAtMs)
264
+ }
265
+ if (entry.revokedAtMs) {
301
266
  return { ok: false, reason: "token-revoked" };
302
- if (entry.token !== params.token)
267
+ }
268
+ if (!verifyPairingToken(params.token, entry.token)) {
303
269
  return { ok: false, reason: "token-mismatch" };
304
- const requestedScopes = normalizeScopes(params.scopes);
305
- if (!scopesAllow(requestedScopes, entry.scopes)) {
270
+ }
271
+ const requestedScopes = normalizeDeviceAuthScopes(params.scopes);
272
+ if (!roleScopesAllow({ role, requestedScopes, allowedScopes: entry.scopes })) {
306
273
  return { ok: false, reason: "scope-mismatch" };
307
274
  }
308
275
  entry.lastUsedAtMs = Date.now();
@@ -316,30 +283,29 @@ export async function verifyDeviceToken(params) {
316
283
  export async function ensureDeviceToken(params) {
317
284
  return await withLock(async () => {
318
285
  const state = await loadState(params.baseDir);
319
- const device = state.pairedByDeviceId[normalizeDeviceId(params.deviceId)];
320
- if (!device)
321
- return null;
322
- const role = normalizeRole(params.role);
323
- if (!role)
286
+ const requestedScopes = normalizeDeviceAuthScopes(params.scopes);
287
+ const context = resolveDeviceTokenUpdateContext({
288
+ state,
289
+ deviceId: params.deviceId,
290
+ role: params.role,
291
+ });
292
+ if (!context) {
324
293
  return null;
325
- const requestedScopes = normalizeScopes(params.scopes);
326
- const tokens = device.tokens ? { ...device.tokens } : {};
327
- const existing = tokens[role];
294
+ }
295
+ const { device, role, tokens, existing } = context;
328
296
  if (existing && !existing.revokedAtMs) {
329
- if (scopesAllow(requestedScopes, existing.scopes)) {
297
+ if (roleScopesAllow({ role, requestedScopes, allowedScopes: existing.scopes })) {
330
298
  return existing;
331
299
  }
332
300
  }
333
301
  const now = Date.now();
334
- const next = {
335
- token: newToken(),
302
+ const next = buildDeviceAuthToken({
336
303
  role,
337
304
  scopes: requestedScopes,
338
- createdAtMs: existing?.createdAtMs ?? now,
305
+ existing,
306
+ now,
339
307
  rotatedAtMs: existing ? now : undefined,
340
- revokedAtMs: undefined,
341
- lastUsedAtMs: existing?.lastUsedAtMs,
342
- };
308
+ });
343
309
  tokens[role] = next;
344
310
  device.tokens = tokens;
345
311
  state.pairedByDeviceId[device.deviceId] = device;
@@ -347,28 +313,40 @@ export async function ensureDeviceToken(params) {
347
313
  return next;
348
314
  });
349
315
  }
316
+ function resolveDeviceTokenUpdateContext(params) {
317
+ const device = getPairedDeviceFromState(params.state, params.deviceId);
318
+ if (!device) {
319
+ return null;
320
+ }
321
+ const role = normalizeRole(params.role);
322
+ if (!role) {
323
+ return null;
324
+ }
325
+ const tokens = cloneDeviceTokens(device);
326
+ const existing = tokens[role];
327
+ return { device, role, tokens, existing };
328
+ }
350
329
  export async function rotateDeviceToken(params) {
351
330
  return await withLock(async () => {
352
331
  const state = await loadState(params.baseDir);
353
- const device = state.pairedByDeviceId[normalizeDeviceId(params.deviceId)];
354
- if (!device)
355
- return null;
356
- const role = normalizeRole(params.role);
357
- if (!role)
332
+ const context = resolveDeviceTokenUpdateContext({
333
+ state,
334
+ deviceId: params.deviceId,
335
+ role: params.role,
336
+ });
337
+ if (!context) {
358
338
  return null;
359
- const tokens = device.tokens ? { ...device.tokens } : {};
360
- const existing = tokens[role];
361
- const requestedScopes = normalizeScopes(params.scopes ?? existing?.scopes ?? device.scopes);
339
+ }
340
+ const { device, role, tokens, existing } = context;
341
+ const requestedScopes = normalizeDeviceAuthScopes(params.scopes ?? existing?.scopes ?? device.scopes);
362
342
  const now = Date.now();
363
- const next = {
364
- token: newToken(),
343
+ const next = buildDeviceAuthToken({
365
344
  role,
366
345
  scopes: requestedScopes,
367
- createdAtMs: existing?.createdAtMs ?? now,
346
+ existing,
347
+ now,
368
348
  rotatedAtMs: now,
369
- revokedAtMs: undefined,
370
- lastUsedAtMs: existing?.lastUsedAtMs,
371
- };
349
+ });
372
350
  tokens[role] = next;
373
351
  device.tokens = tokens;
374
352
  if (params.scopes !== undefined) {
@@ -383,13 +361,16 @@ export async function revokeDeviceToken(params) {
383
361
  return await withLock(async () => {
384
362
  const state = await loadState(params.baseDir);
385
363
  const device = state.pairedByDeviceId[normalizeDeviceId(params.deviceId)];
386
- if (!device)
364
+ if (!device) {
387
365
  return null;
366
+ }
388
367
  const role = normalizeRole(params.role);
389
- if (!role)
368
+ if (!role) {
390
369
  return null;
391
- if (!device.tokens?.[role])
370
+ }
371
+ if (!device.tokens?.[role]) {
392
372
  return null;
373
+ }
393
374
  const tokens = { ...device.tokens };
394
375
  const entry = { ...tokens[role], revokedAtMs: Date.now() };
395
376
  tokens[role] = entry;
@@ -1,31 +1,6 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
1
  import { DEFAULT_SAFE_BINS, analyzeShellCommand, isWindowsPlatform, matchAllowlist, resolveAllowlistCandidatePath, splitCommandChain, } from "./exec-approvals-analysis.js";
2
+ import { SAFE_BIN_GENERIC_PROFILE, SAFE_BIN_PROFILES, validateSafeBinArgv, } from "./exec-safe-bin-policy.js";
4
3
  import { isTrustedSafeBinPath } from "./exec-safe-bin-trust.js";
5
- function isPathLikeToken(value) {
6
- const trimmed = value.trim();
7
- if (!trimmed) {
8
- return false;
9
- }
10
- if (trimmed === "-") {
11
- return false;
12
- }
13
- if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~")) {
14
- return true;
15
- }
16
- if (trimmed.startsWith("/")) {
17
- return true;
18
- }
19
- return /^[A-Za-z]:[\\/]/.test(trimmed);
20
- }
21
- function defaultFileExists(filePath) {
22
- try {
23
- return fs.existsSync(filePath);
24
- }
25
- catch {
26
- return false;
27
- }
28
- }
29
4
  export function normalizeSafeBins(entries) {
30
5
  if (!Array.isArray(entries)) {
31
6
  return new Set();
@@ -41,15 +16,10 @@ export function resolveSafeBins(entries) {
41
16
  }
42
17
  return normalizeSafeBins(entries ?? []);
43
18
  }
44
- function hasGlobToken(value) {
45
- // Safe bins are stdin-only; globbing is both surprising and a historical bypass vector.
46
- // Note: we still harden execution-time expansion separately.
47
- return /[*?[\]]/.test(value);
48
- }
49
19
  export function isSafeBinUsage(params) {
50
20
  // Windows host exec uses PowerShell, which has different parsing/expansion rules.
51
21
  // Keep safeBins conservative there (require explicit allowlist entries).
52
- if (isWindowsPlatform(process.platform)) {
22
+ if (isWindowsPlatform(params.platform ?? process.platform)) {
53
23
  return false;
54
24
  }
55
25
  if (params.safeBins.size === 0) {
@@ -60,55 +30,25 @@ export function isSafeBinUsage(params) {
60
30
  if (!execName) {
61
31
  return false;
62
32
  }
63
- const matchesSafeBin = params.safeBins.has(execName) ||
64
- (process.platform === "win32" && params.safeBins.has(path.parse(execName).name));
33
+ const matchesSafeBin = params.safeBins.has(execName);
65
34
  if (!matchesSafeBin) {
66
35
  return false;
67
36
  }
68
37
  if (!resolution?.resolvedPath) {
69
38
  return false;
70
39
  }
71
- if (!isTrustedSafeBinPath({
40
+ const isTrustedPath = params.isTrustedSafeBinPathFn ?? isTrustedSafeBinPath;
41
+ if (!isTrustedPath({
72
42
  resolvedPath: resolution.resolvedPath,
73
43
  trustedDirs: params.trustedSafeBinDirs,
74
44
  })) {
75
45
  return false;
76
46
  }
77
- const cwd = params.cwd ?? process.cwd();
78
- const exists = params.fileExists ?? defaultFileExists;
79
47
  const argv = params.argv.slice(1);
80
- for (let i = 0; i < argv.length; i += 1) {
81
- const token = argv[i];
82
- if (!token) {
83
- continue;
84
- }
85
- if (token === "-") {
86
- continue;
87
- }
88
- if (token.startsWith("-")) {
89
- const eqIndex = token.indexOf("=");
90
- if (eqIndex > 0) {
91
- const value = token.slice(eqIndex + 1);
92
- if (value && hasGlobToken(value)) {
93
- return false;
94
- }
95
- if (value && (isPathLikeToken(value) || exists(path.resolve(cwd, value)))) {
96
- return false;
97
- }
98
- }
99
- continue;
100
- }
101
- if (hasGlobToken(token)) {
102
- return false;
103
- }
104
- if (isPathLikeToken(token)) {
105
- return false;
106
- }
107
- if (exists(path.resolve(cwd, token))) {
108
- return false;
109
- }
110
- }
111
- return true;
48
+ const safeBinProfiles = params.safeBinProfiles ?? SAFE_BIN_PROFILES;
49
+ const genericSafeBinProfile = params.safeBinGenericProfile ?? SAFE_BIN_GENERIC_PROFILE;
50
+ const profile = safeBinProfiles[execName] ?? genericSafeBinProfile;
51
+ return validateSafeBinArgv(argv, profile);
112
52
  }
113
53
  function evaluateSegments(segments, params) {
114
54
  const matches = [];
@@ -127,7 +67,8 @@ function evaluateSegments(segments, params) {
127
67
  argv: segment.argv,
128
68
  resolution: segment.resolution,
129
69
  safeBins: params.safeBins,
130
- cwd: params.cwd,
70
+ platform: params.platform,
71
+ trustedSafeBinDirs: params.trustedSafeBinDirs,
131
72
  });
132
73
  const skillAllow = allowSkills && segment.resolution?.executableName
133
74
  ? params.skillBins?.has(segment.resolution.executableName)
@@ -157,6 +98,8 @@ export function evaluateExecAllowlist(params) {
157
98
  allowlist: params.allowlist,
158
99
  safeBins: params.safeBins,
159
100
  cwd: params.cwd,
101
+ platform: params.platform,
102
+ trustedSafeBinDirs: params.trustedSafeBinDirs,
160
103
  skillBins: params.skillBins,
161
104
  autoAllowSkills: params.autoAllowSkills,
162
105
  });
@@ -173,6 +116,8 @@ export function evaluateExecAllowlist(params) {
173
116
  allowlist: params.allowlist,
174
117
  safeBins: params.safeBins,
175
118
  cwd: params.cwd,
119
+ platform: params.platform,
120
+ trustedSafeBinDirs: params.trustedSafeBinDirs,
176
121
  skillBins: params.skillBins,
177
122
  autoAllowSkills: params.autoAllowSkills,
178
123
  });
@@ -209,6 +154,8 @@ export function evaluateShellAllowlist(params) {
209
154
  allowlist: params.allowlist,
210
155
  safeBins: params.safeBins,
211
156
  cwd: params.cwd,
157
+ platform: params.platform,
158
+ trustedSafeBinDirs: params.trustedSafeBinDirs,
212
159
  skillBins: params.skillBins,
213
160
  autoAllowSkills: params.autoAllowSkills,
214
161
  });
@@ -239,6 +186,8 @@ export function evaluateShellAllowlist(params) {
239
186
  allowlist: params.allowlist,
240
187
  safeBins: params.safeBins,
241
188
  cwd: params.cwd,
189
+ platform: params.platform,
190
+ trustedSafeBinDirs: params.trustedSafeBinDirs,
242
191
  skillBins: params.skillBins,
243
192
  autoAllowSkills: params.autoAllowSkills,
244
193
  });