@m1a0rz/agent-identity 0.4.6 → 0.5.0

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 (107) hide show
  1. package/README-cn.md +70 -9
  2. package/README.md +70 -9
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +79 -25
  5. package/dist/src/actions/identity-actions.d.ts.map +1 -1
  6. package/dist/src/actions/identity-actions.js +2 -2
  7. package/dist/src/commands/identity-commands.d.ts.map +1 -1
  8. package/dist/src/commands/identity-commands.js +1 -37
  9. package/dist/src/hooks/before-agent-start.d.ts +11 -3
  10. package/dist/src/hooks/before-agent-start.d.ts.map +1 -1
  11. package/dist/src/hooks/before-agent-start.js +12 -11
  12. package/dist/src/hooks/before-dispatch.d.ts +71 -0
  13. package/dist/src/hooks/before-dispatch.d.ts.map +1 -0
  14. package/dist/src/hooks/before-dispatch.js +148 -0
  15. package/dist/src/hooks/before-tool-call.d.ts +7 -2
  16. package/dist/src/hooks/before-tool-call.d.ts.map +1 -1
  17. package/dist/src/hooks/before-tool-call.js +19 -45
  18. package/dist/src/hooks/llm-input.d.ts.map +1 -1
  19. package/dist/src/hooks/llm-input.js +0 -3
  20. package/dist/src/local-server/handlers.d.ts +44 -0
  21. package/dist/src/local-server/handlers.d.ts.map +1 -0
  22. package/dist/src/local-server/handlers.js +207 -0
  23. package/dist/src/local-server/identity-socket.d.ts +18 -0
  24. package/dist/src/local-server/identity-socket.d.ts.map +1 -0
  25. package/dist/src/local-server/identity-socket.js +198 -0
  26. package/dist/src/local-server/peer-check.d.ts +58 -0
  27. package/dist/src/local-server/peer-check.d.ts.map +1 -0
  28. package/dist/src/local-server/peer-check.js +206 -0
  29. package/dist/src/local-server/peercred-linux.d.ts +30 -0
  30. package/dist/src/local-server/peercred-linux.d.ts.map +1 -0
  31. package/dist/src/local-server/peercred-linux.js +69 -0
  32. package/dist/src/risk/llm-risk-check.d.ts +0 -5
  33. package/dist/src/risk/llm-risk-check.d.ts.map +1 -1
  34. package/dist/src/risk/llm-risk-check.js +10 -1
  35. package/dist/src/risk/low-risk-tools.d.ts.map +1 -1
  36. package/dist/src/risk/low-risk-tools.js +0 -1
  37. package/dist/src/store/dispatch-feature-flag.d.ts +7 -0
  38. package/dist/src/store/dispatch-feature-flag.d.ts.map +1 -0
  39. package/dist/src/store/dispatch-feature-flag.js +36 -0
  40. package/dist/src/tools/identity-config-suggest.d.ts +1 -1
  41. package/dist/src/tools/identity-config-suggest.d.ts.map +1 -1
  42. package/dist/src/tools/identity-config-suggest.js +1 -1
  43. package/dist/src/tools/identity-config.d.ts +1 -1
  44. package/dist/src/tools/identity-config.d.ts.map +1 -1
  45. package/dist/src/tools/identity-config.js +1 -1
  46. package/dist/src/tools/identity-fetch.d.ts +1 -1
  47. package/dist/src/tools/identity-fetch.d.ts.map +1 -1
  48. package/dist/src/tools/identity-fetch.js +1 -2
  49. package/dist/src/tools/identity-get-role-credentials.d.ts +1 -1
  50. package/dist/src/tools/identity-get-role-credentials.d.ts.map +1 -1
  51. package/dist/src/tools/identity-get-role-credentials.js +1 -1
  52. package/dist/src/tools/identity-get-session-token.d.ts +1 -1
  53. package/dist/src/tools/identity-get-session-token.d.ts.map +1 -1
  54. package/dist/src/tools/identity-get-session-token.js +1 -1
  55. package/dist/src/tools/identity-get-tip-token.d.ts +1 -1
  56. package/dist/src/tools/identity-get-tip-token.d.ts.map +1 -1
  57. package/dist/src/tools/identity-get-tip-token.js +1 -1
  58. package/dist/src/tools/identity-list-credentials.d.ts +1 -1
  59. package/dist/src/tools/identity-list-credentials.d.ts.map +1 -1
  60. package/dist/src/tools/identity-list-credentials.js +1 -1
  61. package/dist/src/tools/identity-list-risk-patterns.d.ts +1 -1
  62. package/dist/src/tools/identity-list-risk-patterns.d.ts.map +1 -1
  63. package/dist/src/tools/identity-list-risk-patterns.js +1 -1
  64. package/dist/src/tools/identity-list-roles.d.ts +1 -1
  65. package/dist/src/tools/identity-list-roles.d.ts.map +1 -1
  66. package/dist/src/tools/identity-list-roles.js +1 -1
  67. package/dist/src/tools/identity-list-tips.d.ts +1 -1
  68. package/dist/src/tools/identity-list-tips.d.ts.map +1 -1
  69. package/dist/src/tools/identity-list-tips.js +1 -1
  70. package/dist/src/tools/identity-login.d.ts +1 -1
  71. package/dist/src/tools/identity-login.d.ts.map +1 -1
  72. package/dist/src/tools/identity-login.js +1 -1
  73. package/dist/src/tools/identity-logout.d.ts +1 -1
  74. package/dist/src/tools/identity-logout.d.ts.map +1 -1
  75. package/dist/src/tools/identity-logout.js +1 -1
  76. package/dist/src/tools/identity-risk-check.d.ts +1 -1
  77. package/dist/src/tools/identity-risk-check.d.ts.map +1 -1
  78. package/dist/src/tools/identity-risk-check.js +1 -1
  79. package/dist/src/tools/identity-set-binding.d.ts +1 -1
  80. package/dist/src/tools/identity-set-binding.d.ts.map +1 -1
  81. package/dist/src/tools/identity-set-binding.js +1 -1
  82. package/dist/src/tools/identity-status.d.ts +1 -1
  83. package/dist/src/tools/identity-status.d.ts.map +1 -1
  84. package/dist/src/tools/identity-status.js +1 -1
  85. package/dist/src/tools/identity-unset-binding.d.ts +1 -1
  86. package/dist/src/tools/identity-unset-binding.d.ts.map +1 -1
  87. package/dist/src/tools/identity-unset-binding.js +1 -1
  88. package/dist/src/tools/identity-whoami.d.ts +1 -1
  89. package/dist/src/tools/identity-whoami.d.ts.map +1 -1
  90. package/dist/src/tools/identity-whoami.js +1 -1
  91. package/dist/src/types.d.ts +22 -0
  92. package/dist/src/types.d.ts.map +1 -1
  93. package/dist/src/utils/tool-result.d.ts +26 -0
  94. package/dist/src/utils/tool-result.d.ts.map +1 -0
  95. package/dist/src/utils/tool-result.js +40 -0
  96. package/openclaw.plugin.json +15 -0
  97. package/package.json +9 -6
  98. package/skills/SKILL.md +3 -8
  99. package/dist/src/store/tool-approval-store.d.ts +0 -40
  100. package/dist/src/store/tool-approval-store.d.ts.map +0 -1
  101. package/dist/src/store/tool-approval-store.js +0 -162
  102. package/dist/src/tools/identity-approve-tool.d.ts +0 -15
  103. package/dist/src/tools/identity-approve-tool.d.ts.map +0 -1
  104. package/dist/src/tools/identity-approve-tool.js +0 -50
  105. package/dist/src/utils/approval-channel.d.ts +0 -7
  106. package/dist/src/utils/approval-channel.d.ts.map +0 -1
  107. package/dist/src/utils/approval-channel.js +0 -28
@@ -0,0 +1,207 @@
1
+ /*
2
+ * Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { getOrRefreshTIPToken } from "../services/tip-with-refresh.js";
17
+ import { getSessionWithRefresh } from "../services/session-refresh.js";
18
+ import { getAllTIPTokens } from "../store/tip-store.js";
19
+ import { isPersonalSessionModeEnabled, PERSONAL_SESSION_STORAGE_KEY, } from "../store/sender-session-store.js";
20
+ const MAIN_SESSION_KEY = "agent:main:main";
21
+ function resolveMainSessionKey() {
22
+ if (isPersonalSessionModeEnabled())
23
+ return PERSONAL_SESSION_STORAGE_KEY;
24
+ return MAIN_SESSION_KEY;
25
+ }
26
+ function json(res, status, body) {
27
+ res.writeHead(status, { "Content-Type": "application/json" });
28
+ res.end(JSON.stringify(body));
29
+ }
30
+ function parseSessionKeyFromUrl(url) {
31
+ if (!url)
32
+ return null;
33
+ try {
34
+ const u = new URL(url, "http://localhost");
35
+ return u.searchParams.get("session");
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export function createRequestHandler(deps) {
42
+ const { storeDir, identityService, configWorkloadName, getOidcConfigForRefresh, logger } = deps;
43
+ const toolFactories = deps.toolFactories ?? new Map();
44
+ const tipRefreshOptions = {
45
+ identityService,
46
+ getOidcConfigForRefresh,
47
+ configWorkloadName,
48
+ logger,
49
+ };
50
+ const TOOL_PREFIX = "/tool/";
51
+ let cachedToolList = null;
52
+ return async (req, res) => {
53
+ const pathname = req.url?.split("?")[0];
54
+ try {
55
+ if (req.method === "GET" && pathname === "/token") {
56
+ await handleGetToken(req, res);
57
+ }
58
+ else if (req.method === "GET" && pathname === "/session") {
59
+ await handleGetSession(req, res);
60
+ }
61
+ else if (req.method === "GET" && pathname === "/status") {
62
+ handleGetStatus(res);
63
+ }
64
+ else if (req.method === "GET" && pathname === "/tools") {
65
+ handleListTools(res);
66
+ }
67
+ else if (req.method === "POST" && pathname?.startsWith(TOOL_PREFIX)) {
68
+ const toolName = decodeURIComponent(pathname.slice(TOOL_PREFIX.length));
69
+ await handleToolCall(req, res, toolName);
70
+ }
71
+ else {
72
+ json(res, 404, { error: "not_found", message: `Unknown route: ${req.method} ${pathname}` });
73
+ }
74
+ }
75
+ catch (err) {
76
+ logger.warn?.(`local-server: handler error: ${String(err)}`);
77
+ json(res, 500, { error: "internal_error", message: String(err) });
78
+ }
79
+ };
80
+ async function handleGetToken(req, res) {
81
+ const explicitSession = parseSessionKeyFromUrl(req.url);
82
+ const sessionKey = explicitSession ?? resolveMainSessionKey();
83
+ logger.debug?.(`local-server: GET /token sessionKey=${sessionKey.slice(0, 24)}...`);
84
+ const tip = await getOrRefreshTIPToken(storeDir, sessionKey, tipRefreshOptions);
85
+ if (!tip) {
86
+ json(res, 401, {
87
+ error: "no_token",
88
+ message: "No TIP token available for this session. Login required.",
89
+ sessionKey,
90
+ });
91
+ return;
92
+ }
93
+ json(res, 200, {
94
+ tipToken: tip.token,
95
+ sub: tip.sub,
96
+ issuedAt: tip.issuedAt,
97
+ expiresAt: tip.expiresAt,
98
+ sessionKey,
99
+ });
100
+ }
101
+ async function handleGetSession(req, res) {
102
+ const explicitSession = parseSessionKeyFromUrl(req.url);
103
+ const sessionKey = explicitSession ?? resolveMainSessionKey();
104
+ logger.debug?.(`local-server: GET /session sessionKey=${sessionKey.slice(0, 24)}...`);
105
+ const session = await getSessionWithRefresh(storeDir, sessionKey, getOidcConfigForRefresh);
106
+ if (!session?.userToken) {
107
+ json(res, 401, {
108
+ error: "no_session",
109
+ message: "No OIDC session found. Login required.",
110
+ sessionKey,
111
+ });
112
+ return;
113
+ }
114
+ json(res, 200, {
115
+ sessionIdToken: session.userToken,
116
+ sub: session.sub,
117
+ loginAt: session.loginAt,
118
+ expiresAt: session.expiresAt ?? null,
119
+ sessionKey,
120
+ });
121
+ }
122
+ function handleGetStatus(res) {
123
+ const allTips = getAllTIPTokens();
124
+ const now = Date.now();
125
+ const sessions = Object.entries(allTips).map(([key, entry]) => ({
126
+ sessionKey: key,
127
+ sub: entry.sub,
128
+ expiresAt: entry.expiresAt,
129
+ ttlSec: Math.max(0, Math.floor((entry.expiresAt - now) / 1000)),
130
+ }));
131
+ json(res, 200, {
132
+ ok: true,
133
+ personalSessionMode: isPersonalSessionModeEnabled(),
134
+ mainSessionKey: resolveMainSessionKey(),
135
+ activeSessions: sessions.length,
136
+ sessions,
137
+ });
138
+ }
139
+ function handleListTools(res) {
140
+ if (!cachedToolList) {
141
+ cachedToolList = [];
142
+ for (const [, factory] of toolFactories) {
143
+ const tool = factory({});
144
+ cachedToolList.push({
145
+ name: tool.name,
146
+ description: tool.description,
147
+ parameters: tool.parameters,
148
+ });
149
+ }
150
+ cachedToolList.sort((a, b) => a.name.localeCompare(b.name));
151
+ }
152
+ json(res, 200, { tools: cachedToolList });
153
+ }
154
+ async function handleToolCall(req, res, toolName) {
155
+ const factory = toolFactories.get(toolName);
156
+ if (!factory) {
157
+ json(res, 404, { error: "unknown_tool", message: `Tool "${toolName}" not found`, available: Array.from(toolFactories.keys()).sort() });
158
+ return;
159
+ }
160
+ const body = await readBody(req);
161
+ if (body === null) {
162
+ json(res, 400, { error: "bad_request", message: "Invalid JSON body. Expected { params?: {}, session?: string }" });
163
+ return;
164
+ }
165
+ const params = body.params ?? {};
166
+ const sessionKey = typeof body.session === "string" && body.session ? body.session : resolveMainSessionKey();
167
+ const ctx = { sessionKey };
168
+ const tool = factory(ctx);
169
+ const toolCallId = `uds-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
170
+ logger.debug?.(`local-server: POST /tool/${toolName} session=${sessionKey.slice(0, 24)}...`);
171
+ try {
172
+ const result = await tool.execute(toolCallId, params);
173
+ json(res, 200, { tool: toolName, result: result.details ?? null, sessionKey });
174
+ }
175
+ catch (err) {
176
+ logger.warn?.(`local-server: tool ${toolName} error: ${String(err)}`);
177
+ json(res, 500, { error: "tool_error", tool: toolName, message: String(err) });
178
+ }
179
+ }
180
+ }
181
+ /** Read and parse JSON body from an IncomingMessage. Returns null on failure. */
182
+ function readBody(req) {
183
+ return new Promise((resolve) => {
184
+ const chunks = [];
185
+ req.on("data", (chunk) => chunks.push(chunk));
186
+ req.on("end", () => {
187
+ try {
188
+ const raw = Buffer.concat(chunks).toString("utf-8");
189
+ if (!raw.trim()) {
190
+ resolve({});
191
+ return;
192
+ }
193
+ const parsed = JSON.parse(raw);
194
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
195
+ resolve(parsed);
196
+ }
197
+ else {
198
+ resolve(null);
199
+ }
200
+ }
201
+ catch {
202
+ resolve(null);
203
+ }
204
+ });
205
+ req.on("error", () => resolve(null));
206
+ });
207
+ }
@@ -0,0 +1,18 @@
1
+ import { type LocalServerDeps } from "./handlers.js";
2
+ export type IdentitySocketOptions = LocalServerDeps & {
3
+ /** Override socket directory (defaults to storeDir). */
4
+ socketDir?: string;
5
+ /** Additional process names/paths allowed to connect (beyond defaults). */
6
+ allowlist?: readonly string[];
7
+ /**
8
+ * When peer resolution fails (lsof unavailable, timing race, etc.),
9
+ * allow the request through with a warning log. Default: true.
10
+ */
11
+ failOpen?: boolean;
12
+ };
13
+ export declare function startIdentitySocket(opts: IdentitySocketOptions): Promise<string>;
14
+ export declare function stopIdentitySocket(logger?: {
15
+ info?: (msg: string) => void;
16
+ }): Promise<void>;
17
+ export declare function getSocketPath(): string | null;
18
+ //# sourceMappingURL=identity-socket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-socket.d.ts","sourceRoot":"","sources":["../../../src/local-server/identity-socket.ts"],"names":[],"mappings":"AAqCA,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAM3E,MAAM,MAAM,qBAAqB,GAAG,eAAe,GAAG;IACpD,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA2EF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkGhF;AAED,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB3F;AAED,wBAAgB,aAAa,IAAI,MAAM,GAAG,IAAI,CAE7C"}
@@ -0,0 +1,198 @@
1
+ /*
2
+ * Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * HTTP-over-UDS server for local identity token access.
18
+ *
19
+ * Other processes on the same machine can connect to the Unix domain socket
20
+ * to retrieve TIP tokens and session info without network exposure.
21
+ *
22
+ * Security layers:
23
+ * 1. Socket file mode 0600 — only the same OS user can connect.
24
+ * 2. Peer process check — uses `lsof` to identify the connecting process
25
+ * and validates it against an allowlist (default: curl, wget, python, etc.).
26
+ *
27
+ * Lifecycle:
28
+ * - startIdentitySocket() creates the server and listens on the socket path.
29
+ * - stopIdentitySocket() gracefully shuts down and removes the socket file.
30
+ * - Stale socket files from crashed processes are cleaned up on start.
31
+ */
32
+ import { createServer } from "node:http";
33
+ import fs from "node:fs";
34
+ import path from "node:path";
35
+ import { createRequestHandler } from "./handlers.js";
36
+ import { checkPeer, registerPeerCredProvider, hasPeerCredProvider, setPeerCheckLogger } from "./peer-check.js";
37
+ import { tryBuildPeerCredProvider } from "./peercred-linux.js";
38
+ const SOCKET_FILENAME = "identity.sock";
39
+ let server = null;
40
+ let currentSocketPath = null;
41
+ function resolveSocketPath(opts) {
42
+ const dir = opts.socketDir ?? opts.storeDir;
43
+ return path.join(dir, SOCKET_FILENAME);
44
+ }
45
+ function cleanupStaleSocket(socketPath) {
46
+ try {
47
+ const stat = fs.statSync(socketPath);
48
+ if (stat.isSocket()) {
49
+ fs.unlinkSync(socketPath);
50
+ }
51
+ }
52
+ catch (err) {
53
+ if (err.code !== "ENOENT") {
54
+ throw err;
55
+ }
56
+ }
57
+ }
58
+ function ensureDirectory(dir) {
59
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
60
+ }
61
+ // ─── Per-connection peer info cache ──────────────────────────────────
62
+ const peerCache = new WeakMap();
63
+ /** Extract the raw fd from a Node.js Socket for SO_PEERCRED. */
64
+ function getSocketFd(socket) {
65
+ // Node.js internal: _handle is a Pipe (libuv) wrapping the fd.
66
+ // Safe to access — returns -1 if unavailable so checkPeer falls back.
67
+ const handle = socket._handle;
68
+ return typeof handle?.fd === "number" ? handle.fd : -1;
69
+ }
70
+ function resolvePeerForSocket(socket, allowlist, failOpen, logger) {
71
+ const cached = peerCache.get(socket);
72
+ if (cached?.checked) {
73
+ return { allowed: true, peer: cached.info };
74
+ }
75
+ const fd = getSocketFd(socket);
76
+ const result = checkPeer(allowlist, failOpen, fd);
77
+ peerCache.set(socket, { info: result.peer ?? null, checked: true });
78
+ if (!result.allowed) {
79
+ logger.warn?.(`local-server: REJECTED connection: ${result.reason}`);
80
+ return { allowed: false, peer: result.peer ?? null };
81
+ }
82
+ if (result.peer) {
83
+ logger.debug?.(`local-server: accepted connection from ${result.peer.processName} ` +
84
+ `(pid=${result.peer.pid}, path=${result.peer.processPath})`);
85
+ }
86
+ else {
87
+ logger.debug?.("local-server: accepted connection (peer unresolvable, fail-open)");
88
+ }
89
+ return { allowed: true, peer: result.peer ?? null };
90
+ }
91
+ // ─── Server lifecycle ────────────────────────────────────────────────
92
+ export function startIdentitySocket(opts) {
93
+ if (server) {
94
+ opts.logger.warn?.("local-server: already running, skipping duplicate start");
95
+ return Promise.resolve(currentSocketPath);
96
+ }
97
+ const socketPath = resolveSocketPath(opts);
98
+ const socketDir = path.dirname(socketPath);
99
+ const allowlist = opts.allowlist ?? [];
100
+ const failOpen = opts.failOpen ?? true;
101
+ ensureDirectory(socketDir);
102
+ cleanupStaleSocket(socketPath);
103
+ const handler = createRequestHandler(opts);
104
+ // Wire peer-check debug/warn to the plugin logger
105
+ setPeerCheckLogger({
106
+ debug: (msg) => opts.logger.debug?.(msg),
107
+ warn: (msg) => opts.logger.warn?.(msg),
108
+ });
109
+ // Auto-register SO_PEERCRED provider via koffi (Linux only, optional)
110
+ if (!hasPeerCredProvider()) {
111
+ const provider = tryBuildPeerCredProvider(opts.logger);
112
+ if (provider) {
113
+ registerPeerCredProvider(provider);
114
+ opts.logger.info?.("local-server: SO_PEERCRED provider registered via koffi");
115
+ }
116
+ else {
117
+ opts.logger.debug?.("local-server: SO_PEERCRED provider not available, using fail-open policy");
118
+ }
119
+ }
120
+ server = createServer((req, res) => {
121
+ const method = req.method ?? "?";
122
+ const url = req.url ?? "/";
123
+ // ── Peer process check ────────────────────────────────────────
124
+ const { allowed, peer } = resolvePeerForSocket(req.socket, allowlist, failOpen, opts.logger);
125
+ if (!allowed) {
126
+ const peerDesc = peer
127
+ ? `${peer.processName} (pid=${peer.pid}, uid=${peer.uid}, path=${peer.processPath})`
128
+ : "unknown";
129
+ opts.logger.warn?.(`local-server: REJECTED ${method} ${url} from ${peerDesc}`);
130
+ res.writeHead(403, { "Content-Type": "application/json" });
131
+ res.end(JSON.stringify({
132
+ error: "forbidden",
133
+ message: "Process not in allowlist. Configure identity.localServerAllowlist to grant access.",
134
+ process: peer ? {
135
+ pid: peer.pid,
136
+ name: peer.processName,
137
+ path: peer.processPath,
138
+ } : null,
139
+ }));
140
+ return;
141
+ }
142
+ const peerDesc = peer
143
+ ? `${peer.processName}(pid=${peer.pid})`
144
+ : "unknown-peer";
145
+ opts.logger.debug?.(`local-server: ${method} ${url} from ${peerDesc}`);
146
+ handler(req, res).catch((err) => {
147
+ opts.logger.warn?.(`local-server: unhandled error on ${method} ${url}: ${String(err)}`);
148
+ if (!res.headersSent) {
149
+ res.writeHead(500, { "Content-Type": "application/json" });
150
+ res.end(JSON.stringify({ error: "internal_error" }));
151
+ }
152
+ });
153
+ });
154
+ server.unref();
155
+ return new Promise((resolve, reject) => {
156
+ server.on("error", (err) => {
157
+ opts.logger.warn?.(`local-server: server error: ${String(err)}`);
158
+ reject(err);
159
+ });
160
+ server.listen(socketPath, () => {
161
+ try {
162
+ fs.chmodSync(socketPath, 0o600);
163
+ }
164
+ catch (err) {
165
+ opts.logger.warn?.(`local-server: failed to chmod socket: ${String(err)}`);
166
+ }
167
+ currentSocketPath = socketPath;
168
+ opts.logger.info?.(`local-server: listening on ${socketPath} (peer check enabled, fail-open=${failOpen})`);
169
+ resolve(socketPath);
170
+ });
171
+ });
172
+ }
173
+ export function stopIdentitySocket(logger) {
174
+ return new Promise((resolve) => {
175
+ if (!server) {
176
+ resolve();
177
+ return;
178
+ }
179
+ const sock = currentSocketPath;
180
+ server.close(() => {
181
+ if (sock) {
182
+ try {
183
+ fs.unlinkSync(sock);
184
+ }
185
+ catch {
186
+ // already removed
187
+ }
188
+ }
189
+ logger?.info?.(`local-server: stopped (socket ${sock ?? "unknown"})`);
190
+ server = null;
191
+ currentSocketPath = null;
192
+ resolve();
193
+ });
194
+ });
195
+ }
196
+ export function getSocketPath() {
197
+ return currentSocketPath;
198
+ }
@@ -0,0 +1,58 @@
1
+ export type PeerCheckLogger = {
2
+ debug?: (msg: string) => void;
3
+ warn?: (msg: string) => void;
4
+ };
5
+ export declare function setPeerCheckLogger(logger: PeerCheckLogger): void;
6
+ export type PeerInfo = {
7
+ pid: number;
8
+ uid: number;
9
+ gid: number;
10
+ processName: string;
11
+ processPath: string;
12
+ };
13
+ export type PeerCredentials = {
14
+ pid: number;
15
+ uid: number;
16
+ gid: number;
17
+ };
18
+ export type GetPeerCredFn = (fd: number) => PeerCredentials | null;
19
+ /**
20
+ * Register a native SO_PEERCRED provider. Call this at startup if a native
21
+ * binding (N-API addon, koffi FFI, etc.) is available.
22
+ *
23
+ * Example with a hypothetical native addon:
24
+ * import { getPeerCred } from "./peercred.node";
25
+ * registerPeerCredProvider(getPeerCred);
26
+ *
27
+ * The provider receives the connected socket fd and must return
28
+ * { pid, uid, gid } or null on failure.
29
+ */
30
+ export declare function registerPeerCredProvider(fn: GetPeerCredFn): void;
31
+ /** True when a native SO_PEERCRED provider is registered. */
32
+ export declare function hasPeerCredProvider(): boolean;
33
+ /**
34
+ * Check whether a peer process is in the allowlist.
35
+ *
36
+ * Matching rules (any match = allowed):
37
+ * 1. Exact basename match: "curl" matches /usr/bin/curl
38
+ * 2. Full path match: "/usr/bin/curl" matches exactly
39
+ * 3. Glob suffix: "python*" matches python3, python3.11
40
+ */
41
+ export declare function isProcessAllowed(peer: PeerInfo, customAllowlist?: readonly string[]): boolean;
42
+ /**
43
+ * Resolve and validate the peer process for a UDS connection.
44
+ *
45
+ * @param customAllowlist Extra process names/paths to allow.
46
+ * @param failOpen If true, allow when SO_PEERCRED is unavailable
47
+ * (socket 0600 still provides UID-level protection).
48
+ * @param socketFd The accepted connection socket's fd (for SO_PEERCRED).
49
+ */
50
+ export declare function checkPeer(customAllowlist: readonly string[], failOpen: boolean, socketFd?: number): {
51
+ allowed: true;
52
+ peer: PeerInfo | null;
53
+ } | {
54
+ allowed: false;
55
+ reason: string;
56
+ peer?: PeerInfo;
57
+ };
58
+ //# sourceMappingURL=peer-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-check.d.ts","sourceRoot":"","sources":["../../../src/local-server/peer-check.ts"],"names":[],"mappings":"AA+CA,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B,CAAC;AAIF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAEhE;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AACxE,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,eAAe,GAAG,IAAI,CAAC;AAoBnE;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,aAAa,GAAG,IAAI,CAEhE;AAED,6DAA6D;AAC7D,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAoED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,QAAQ,EACd,eAAe,GAAE,SAAS,MAAM,EAAO,GACtC,OAAO,CAUT;AAID;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACvB,eAAe,EAAE,SAAS,MAAM,EAAE,EAClC,QAAQ,EAAE,OAAO,EACjB,QAAQ,GAAE,MAAW,GACpB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,QAAQ,CAAA;CAAE,CAiChG"}