@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.
- package/CHANGELOG.md +29 -0
- package/dist/acp/client.js +207 -18
- package/dist/acp/secret-file.js +22 -0
- package/dist/agents/agent-scope.js +10 -0
- package/dist/agents/bash-process-registry.test-helpers.js +29 -0
- package/dist/agents/bash-tools.exec-approval-request.js +20 -0
- package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
- package/dist/agents/bash-tools.exec-host-node.js +235 -0
- package/dist/agents/bash-tools.exec-types.js +1 -0
- package/dist/agents/bash-tools.process.js +224 -218
- package/dist/agents/content-blocks.js +16 -0
- package/dist/agents/model-fallback.js +96 -101
- package/dist/agents/models-config.providers.js +299 -182
- package/dist/agents/pi-embedded-payloads.js +1 -0
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
- package/dist/agents/skills.test-helpers.js +13 -0
- package/dist/agents/stable-stringify.js +12 -0
- package/dist/agents/subagent-registry.mocks.shared.js +12 -0
- package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
- package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
- package/dist/agents/tool-policy-shared.js +108 -0
- package/dist/agents/tools/browser-tool.js +160 -54
- package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
- package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
- package/dist/agents/tools/image-tool.js +214 -99
- package/dist/agents/tools/sessions-history-tool.js +140 -108
- package/dist/agents/workspace.js +222 -46
- package/dist/auto-reply/commands-registry.js +15 -18
- package/dist/auto-reply/fallback-state.js +114 -0
- package/dist/auto-reply/model-runtime.js +68 -0
- package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
- package/dist/auto-reply/reply/agent-runner.js +165 -39
- package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
- package/dist/browser/config.js +26 -0
- package/dist/browser/navigation-guard.js +31 -0
- package/dist/browser/routes/agent.act.js +431 -424
- package/dist/browser/routes/agent.shared.js +47 -3
- package/dist/browser/routes/agent.snapshot.js +122 -116
- package/dist/browser/routes/agent.storage.js +303 -297
- package/dist/browser/routes/tabs.js +154 -100
- package/dist/browser/server-lifecycle.js +37 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/allow-from.js +25 -0
- package/dist/channels/plugins/account-action-gate.js +13 -0
- package/dist/channels/plugins/message-actions.js +10 -0
- package/dist/channels/telegram/api.js +18 -0
- package/dist/cli/argv.js +84 -21
- package/dist/cli/banner.js +2 -1
- package/dist/cli/exec-approvals-cli.js +92 -124
- package/dist/cli/memory-cli.js +158 -61
- package/dist/cli/nodes-cli/register.push.js +63 -0
- package/dist/cli/nodes-media-utils.js +21 -0
- package/dist/cli/plugins-cli.js +245 -61
- package/dist/cli/program/build-program.js +3 -1
- package/dist/cli/program/command-registry.js +223 -136
- package/dist/cli/program/help.js +43 -12
- package/dist/cli/route.js +1 -1
- package/dist/cli/test-runtime-capture.js +24 -0
- package/dist/commands/agent.js +163 -87
- package/dist/commands/channels.mock-harness.js +23 -0
- package/dist/commands/daemon-install-runtime-warning.js +11 -0
- package/dist/commands/onboard-helpers.js +4 -4
- package/dist/commands/sessions.test-helpers.js +61 -0
- package/dist/compat/legacy-names.js +2 -2
- package/dist/config/commands.js +3 -0
- package/dist/config/config.js +1 -1
- package/dist/config/env-substitution.js +62 -34
- package/dist/config/env-vars.js +9 -0
- package/dist/config/io.js +571 -171
- package/dist/config/merge-patch.js +50 -4
- package/dist/config/redact-snapshot.js +404 -76
- package/dist/config/schema.js +58 -570
- package/dist/config/validation.js +140 -85
- package/dist/config/zod-schema.hooks.js +40 -11
- package/dist/config/zod-schema.installs.js +20 -0
- package/dist/config/zod-schema.js +8 -7
- package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
- package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
- package/dist/control-ui/index.html +1 -1
- package/dist/daemon/cmd-argv.js +21 -0
- package/dist/daemon/cmd-set.js +58 -0
- package/dist/daemon/service-types.js +1 -0
- package/dist/discord/monitor/exec-approvals.js +357 -162
- package/dist/gateway/auth.js +38 -3
- package/dist/gateway/call.js +149 -68
- package/dist/gateway/canvas-capability.js +75 -0
- package/dist/gateway/control-plane-audit.js +28 -0
- package/dist/gateway/control-plane-rate-limit.js +53 -0
- package/dist/gateway/events.js +1 -0
- package/dist/gateway/hooks.js +109 -54
- package/dist/gateway/http-common.js +22 -0
- package/dist/gateway/method-scopes.js +169 -0
- package/dist/gateway/net.js +23 -0
- package/dist/gateway/openresponses-http.js +120 -110
- package/dist/gateway/probe-auth.js +2 -0
- package/dist/gateway/protocol/index.js +3 -2
- package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
- package/dist/gateway/protocol/schema/push.js +18 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-http.js +236 -52
- package/dist/gateway/server-methods/agent.js +162 -24
- package/dist/gateway/server-methods/chat.js +461 -130
- package/dist/gateway/server-methods/config.js +193 -150
- package/dist/gateway/server-methods/nodes.helpers.js +12 -0
- package/dist/gateway/server-methods/nodes.js +251 -69
- package/dist/gateway/server-methods/push.js +53 -0
- package/dist/gateway/server-reload-handlers.js +2 -3
- package/dist/gateway/server-runtime-config.js +5 -0
- package/dist/gateway/server-runtime-state.js +2 -0
- package/dist/gateway/server-ws-runtime.js +1 -0
- package/dist/gateway/server.impl.js +296 -139
- package/dist/gateway/session-preview.test-helpers.js +11 -0
- package/dist/gateway/startup-auth.js +126 -0
- package/dist/gateway/test-helpers.agent-results.js +15 -0
- package/dist/gateway/test-helpers.mocks.js +37 -14
- package/dist/gateway/test-helpers.server.js +161 -77
- package/dist/hooks/bundled/session-memory/handler.js +165 -34
- package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
- package/dist/infra/archive-path.js +49 -0
- package/dist/infra/device-pairing.js +148 -167
- package/dist/infra/exec-approvals-allowlist.js +19 -70
- package/dist/infra/exec-approvals-analysis.js +44 -17
- package/dist/infra/exec-safe-bin-policy.js +269 -0
- package/dist/infra/fixed-window-rate-limit.js +33 -0
- package/dist/infra/git-root.js +61 -0
- package/dist/infra/heartbeat-active-hours.js +2 -2
- package/dist/infra/heartbeat-reason.js +40 -0
- package/dist/infra/heartbeat-runner.js +72 -32
- package/dist/infra/install-source-utils.js +91 -7
- package/dist/infra/node-pairing.js +50 -105
- package/dist/infra/npm-integrity.js +45 -0
- package/dist/infra/npm-pack-install.js +40 -0
- package/dist/infra/outbound/channel-adapters.js +20 -7
- package/dist/infra/outbound/message-action-runner.js +107 -327
- package/dist/infra/outbound/message.js +59 -36
- package/dist/infra/outbound/outbound-policy.js +52 -25
- package/dist/infra/outbound/outbound-send-service.js +58 -71
- package/dist/infra/pairing-files.js +10 -0
- package/dist/infra/plain-object.js +9 -0
- package/dist/infra/push-apns.js +365 -0
- package/dist/infra/restart-sentinel.js +16 -1
- package/dist/infra/restart.js +229 -26
- package/dist/infra/scp-host.js +54 -0
- package/dist/infra/update-startup.js +86 -9
- package/dist/media/inbound-path-policy.js +114 -0
- package/dist/media/input-files.js +16 -0
- package/dist/memory/test-manager.js +8 -0
- package/dist/plugin-sdk/temp-path.js +47 -0
- package/dist/plugins/discovery.js +217 -23
- package/dist/plugins/hook-runner-global.js +16 -0
- package/dist/plugins/loader.js +192 -26
- package/dist/plugins/logger.js +8 -0
- package/dist/plugins/manifest-registry.js +3 -0
- package/dist/plugins/path-safety.js +34 -0
- package/dist/plugins/registry.js +5 -2
- package/dist/plugins/runtime/index.js +271 -206
- package/dist/providers/github-copilot-models.js +4 -1
- package/dist/security/audit-channel.js +8 -19
- package/dist/security/audit-extra.async.js +354 -182
- package/dist/security/audit-extra.js +11 -1
- package/dist/security/audit-extra.sync.js +340 -33
- package/dist/security/audit-fs.js +31 -13
- package/dist/security/audit.js +145 -371
- package/dist/security/dm-policy-shared.js +24 -0
- package/dist/security/external-content.js +20 -8
- package/dist/security/fix.js +49 -85
- package/dist/security/scan-paths.js +20 -0
- package/dist/security/secret-equal.js +3 -7
- package/dist/security/windows-acl.js +30 -15
- package/dist/shared/node-list-parse.js +13 -0
- package/dist/shared/operator-scope-compat.js +37 -0
- package/dist/shared/text-chunking.js +29 -0
- package/dist/slack/blocks.test-helpers.js +31 -0
- package/dist/slack/monitor/mrkdwn.js +8 -0
- package/dist/telegram/bot-message-dispatch.js +366 -164
- package/dist/telegram/draft-stream.js +30 -7
- package/dist/telegram/reasoning-lane-coordinator.js +128 -0
- package/dist/terminal/prompt-select-styled.js +9 -0
- package/dist/test-utils/command-runner.js +6 -0
- package/dist/test-utils/internal-hook-event-payload.js +10 -0
- package/dist/test-utils/model-auth-mock.js +12 -0
- package/dist/test-utils/provider-usage-fetch.js +14 -0
- package/dist/test-utils/temp-home.js +33 -0
- package/dist/tui/components/chat-log.js +9 -0
- package/dist/tui/tui-command-handlers.js +36 -27
- package/dist/tui/tui-event-handlers.js +122 -32
- package/dist/tui/tui.js +181 -45
- package/dist/utils/mask-api-key.js +10 -0
- package/dist/utils/run-with-concurrency.js +39 -0
- package/dist/web/media.js +4 -0
- package/docs/tools/slash-commands.md +5 -1
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/feishu/src/external-keys.ts +19 -0
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/lobster/src/windows-spawn.ts +193 -0
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
- package/extensions/mattermost/package.json +1 -1
- package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
|
@@ -1,85 +1,28 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
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
|
-
|
|
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 } =
|
|
9
|
+
const { pendingPath, pairedPath } = resolvePairingPaths(baseDir, "devices");
|
|
67
10
|
const [pending, paired] = await Promise.all([
|
|
68
|
-
|
|
69
|
-
|
|
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 } =
|
|
22
|
+
const { pendingPath, pairedPath } = resolvePairingPaths(baseDir, "devices");
|
|
80
23
|
await Promise.all([
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
130
|
-
|
|
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
|
|
141
|
-
|
|
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
|
|
149
|
-
return
|
|
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).
|
|
154
|
-
const paired = Object.values(state.pairedByDeviceId).
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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 =
|
|
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
|
-
.
|
|
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
|
|
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
|
-
|
|
264
|
+
}
|
|
265
|
+
if (entry.revokedAtMs) {
|
|
301
266
|
return { ok: false, reason: "token-revoked" };
|
|
302
|
-
|
|
267
|
+
}
|
|
268
|
+
if (!verifyPairingToken(params.token, entry.token)) {
|
|
303
269
|
return { ok: false, reason: "token-mismatch" };
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
const
|
|
327
|
-
const existing = tokens[role];
|
|
294
|
+
}
|
|
295
|
+
const { device, role, tokens, existing } = context;
|
|
328
296
|
if (existing && !existing.revokedAtMs) {
|
|
329
|
-
if (
|
|
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
|
-
|
|
305
|
+
existing,
|
|
306
|
+
now,
|
|
339
307
|
rotatedAtMs: existing ? now : undefined,
|
|
340
|
-
|
|
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
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
332
|
+
const context = resolveDeviceTokenUpdateContext({
|
|
333
|
+
state,
|
|
334
|
+
deviceId: params.deviceId,
|
|
335
|
+
role: params.role,
|
|
336
|
+
});
|
|
337
|
+
if (!context) {
|
|
358
338
|
return null;
|
|
359
|
-
|
|
360
|
-
const existing =
|
|
361
|
-
const requestedScopes =
|
|
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
|
-
|
|
346
|
+
existing,
|
|
347
|
+
now,
|
|
368
348
|
rotatedAtMs: now,
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
});
|