@m1a0rz/agent-identity 0.1.2

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 (125) hide show
  1. package/README-cn.md +223 -0
  2. package/README.md +223 -0
  3. package/dist/index.d.ts +14 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +306 -0
  6. package/dist/src/actions/identity-actions.d.ts +142 -0
  7. package/dist/src/actions/identity-actions.d.ts.map +1 -0
  8. package/dist/src/actions/identity-actions.js +429 -0
  9. package/dist/src/commands/identity-commands.d.ts +33 -0
  10. package/dist/src/commands/identity-commands.d.ts.map +1 -0
  11. package/dist/src/commands/identity-commands.js +572 -0
  12. package/dist/src/hooks/after-tool-call.d.ts +22 -0
  13. package/dist/src/hooks/after-tool-call.d.ts.map +1 -0
  14. package/dist/src/hooks/after-tool-call.js +35 -0
  15. package/dist/src/hooks/before-agent-start.d.ts +30 -0
  16. package/dist/src/hooks/before-agent-start.d.ts.map +1 -0
  17. package/dist/src/hooks/before-agent-start.js +93 -0
  18. package/dist/src/hooks/before-tool-call.d.ts +38 -0
  19. package/dist/src/hooks/before-tool-call.d.ts.map +1 -0
  20. package/dist/src/hooks/before-tool-call.js +138 -0
  21. package/dist/src/risk/classify-risk.d.ts +24 -0
  22. package/dist/src/risk/classify-risk.d.ts.map +1 -0
  23. package/dist/src/risk/classify-risk.js +61 -0
  24. package/dist/src/risk/diagnose-risk.d.ts +21 -0
  25. package/dist/src/risk/diagnose-risk.d.ts.map +1 -0
  26. package/dist/src/risk/diagnose-risk.js +37 -0
  27. package/dist/src/risk/llm-risk-check.d.ts +27 -0
  28. package/dist/src/risk/llm-risk-check.d.ts.map +1 -0
  29. package/dist/src/risk/llm-risk-check.js +274 -0
  30. package/dist/src/risk/low-risk-tools.d.ts +5 -0
  31. package/dist/src/risk/low-risk-tools.d.ts.map +1 -0
  32. package/dist/src/risk/low-risk-tools.js +29 -0
  33. package/dist/src/routes/oidc-login.d.ts +51 -0
  34. package/dist/src/routes/oidc-login.d.ts.map +1 -0
  35. package/dist/src/routes/oidc-login.js +153 -0
  36. package/dist/src/services/identity-client.d.ts +366 -0
  37. package/dist/src/services/identity-client.d.ts.map +1 -0
  38. package/dist/src/services/identity-client.js +578 -0
  39. package/dist/src/services/identity-credentials.d.ts +28 -0
  40. package/dist/src/services/identity-credentials.d.ts.map +1 -0
  41. package/dist/src/services/identity-credentials.js +170 -0
  42. package/dist/src/services/identity-service.d.ts +33 -0
  43. package/dist/src/services/identity-service.d.ts.map +1 -0
  44. package/dist/src/services/identity-service.js +53 -0
  45. package/dist/src/services/oidc-client.d.ts +57 -0
  46. package/dist/src/services/oidc-client.d.ts.map +1 -0
  47. package/dist/src/services/oidc-client.js +127 -0
  48. package/dist/src/services/send-notification-feishu.d.ts +27 -0
  49. package/dist/src/services/send-notification-feishu.d.ts.map +1 -0
  50. package/dist/src/services/send-notification-feishu.js +148 -0
  51. package/dist/src/services/session-refresh.d.ts +16 -0
  52. package/dist/src/services/session-refresh.d.ts.map +1 -0
  53. package/dist/src/services/session-refresh.js +38 -0
  54. package/dist/src/store/credential-env-bindings.d.ts +16 -0
  55. package/dist/src/store/credential-env-bindings.d.ts.map +1 -0
  56. package/dist/src/store/credential-env-bindings.js +61 -0
  57. package/dist/src/store/credential-store.d.ts +31 -0
  58. package/dist/src/store/credential-store.d.ts.map +1 -0
  59. package/dist/src/store/credential-store.js +57 -0
  60. package/dist/src/store/oidc-state-store.d.ts +15 -0
  61. package/dist/src/store/oidc-state-store.d.ts.map +1 -0
  62. package/dist/src/store/oidc-state-store.js +32 -0
  63. package/dist/src/store/session-store.d.ts +21 -0
  64. package/dist/src/store/session-store.d.ts.map +1 -0
  65. package/dist/src/store/session-store.js +69 -0
  66. package/dist/src/store/tip-store.d.ts +21 -0
  67. package/dist/src/store/tip-store.d.ts.map +1 -0
  68. package/dist/src/store/tip-store.js +60 -0
  69. package/dist/src/store/tool-approval-store.d.ts +44 -0
  70. package/dist/src/store/tool-approval-store.d.ts.map +1 -0
  71. package/dist/src/store/tool-approval-store.js +147 -0
  72. package/dist/src/tools/identity-approve-tool.d.ts +24 -0
  73. package/dist/src/tools/identity-approve-tool.d.ts.map +1 -0
  74. package/dist/src/tools/identity-approve-tool.js +36 -0
  75. package/dist/src/tools/identity-config.d.ts +13 -0
  76. package/dist/src/tools/identity-config.d.ts.map +1 -0
  77. package/dist/src/tools/identity-config.js +18 -0
  78. package/dist/src/tools/identity-fetch.d.ts +21 -0
  79. package/dist/src/tools/identity-fetch.d.ts.map +1 -0
  80. package/dist/src/tools/identity-fetch.js +63 -0
  81. package/dist/src/tools/identity-list-credentials.d.ts +15 -0
  82. package/dist/src/tools/identity-list-credentials.d.ts.map +1 -0
  83. package/dist/src/tools/identity-list-credentials.js +30 -0
  84. package/dist/src/tools/identity-list-risk-patterns.d.ts +13 -0
  85. package/dist/src/tools/identity-list-risk-patterns.d.ts.map +1 -0
  86. package/dist/src/tools/identity-list-risk-patterns.js +23 -0
  87. package/dist/src/tools/identity-list-tips.d.ts +13 -0
  88. package/dist/src/tools/identity-list-tips.d.ts.map +1 -0
  89. package/dist/src/tools/identity-list-tips.js +21 -0
  90. package/dist/src/tools/identity-login.d.ts +14 -0
  91. package/dist/src/tools/identity-login.d.ts.map +1 -0
  92. package/dist/src/tools/identity-login.js +40 -0
  93. package/dist/src/tools/identity-logout.d.ts +13 -0
  94. package/dist/src/tools/identity-logout.d.ts.map +1 -0
  95. package/dist/src/tools/identity-logout.js +24 -0
  96. package/dist/src/tools/identity-risk-check.d.ts +29 -0
  97. package/dist/src/tools/identity-risk-check.d.ts.map +1 -0
  98. package/dist/src/tools/identity-risk-check.js +54 -0
  99. package/dist/src/tools/identity-set-binding.d.ts +16 -0
  100. package/dist/src/tools/identity-set-binding.d.ts.map +1 -0
  101. package/dist/src/tools/identity-set-binding.js +31 -0
  102. package/dist/src/tools/identity-status.d.ts +13 -0
  103. package/dist/src/tools/identity-status.d.ts.map +1 -0
  104. package/dist/src/tools/identity-status.js +41 -0
  105. package/dist/src/tools/identity-unset-binding.d.ts +15 -0
  106. package/dist/src/tools/identity-unset-binding.d.ts.map +1 -0
  107. package/dist/src/tools/identity-unset-binding.js +25 -0
  108. package/dist/src/tools/identity-whoami.d.ts +13 -0
  109. package/dist/src/tools/identity-whoami.d.ts.map +1 -0
  110. package/dist/src/tools/identity-whoami.js +38 -0
  111. package/dist/src/types.d.ts +93 -0
  112. package/dist/src/types.d.ts.map +1 -0
  113. package/dist/src/types.js +5 -0
  114. package/dist/src/utils/approval-channel.d.ts +11 -0
  115. package/dist/src/utils/approval-channel.d.ts.map +1 -0
  116. package/dist/src/utils/approval-channel.js +13 -0
  117. package/dist/src/utils/auth.d.ts +24 -0
  118. package/dist/src/utils/auth.d.ts.map +1 -0
  119. package/dist/src/utils/auth.js +44 -0
  120. package/dist/src/utils/derive-session-key.d.ts +78 -0
  121. package/dist/src/utils/derive-session-key.d.ts.map +1 -0
  122. package/dist/src/utils/derive-session-key.js +198 -0
  123. package/openclaw.plugin.json +162 -0
  124. package/package.json +33 -0
  125. package/skills/SKILL.md +230 -0
@@ -0,0 +1,572 @@
1
+ /**
2
+ * Unified /identity command: login, status, logout, list-credentials, fetch, set.
3
+ * UserPool OIDC + credential hosting. Uses shared identity-actions for logic.
4
+ */
5
+ import { runStatus, runLogin, runLogout, runListCredentials, runListTips, runConfig, runFetch, runSetBinding, runUnsetBinding, } from "../actions/identity-actions.js";
6
+ import { deriveSessionKey, deriveDeliveryTargetFromContext, } from "../utils/derive-session-key.js";
7
+ import { diagnoseRisk } from "../risk/diagnose-risk.js";
8
+ import { getRiskPatterns } from "../risk/classify-risk.js";
9
+ import * as toolApprovalStore from "../store/tool-approval-store.js";
10
+ const HELP_TEXT = `**/identity** – UserPool login, TIP token, credentials, and risk approval
11
+
12
+ Subcommands:
13
+ • \`whoami\` – Identity brief: sub, login time, TIP expiry
14
+ • \`status\` – Full status: session/TIP details (issued, expires, chain), credentials, bindings
15
+ • \`login\` – Log in via OIDC (returns auth URL if not logged in)
16
+ • \`logout\` – Clear session and TIP
17
+ • \`list-credentials\` or \`list [page]\` – List providers and credentials (paginated; shows bound env)
18
+ • \`list-tips\` – List all valid TIP tokens with delegation chain and bindings
19
+ • \`config\` – Show identity plugin configuration (redacted)
20
+ • \`fetch <provider> [--flow=oauth2-user|oauth2-m2m|apikey] [--redirectUrl=<url>] [--scopes=a,b]\` – Add credential
21
+ • \`set <provider> <envVar>\` – Bind credential to env var for tool injection
22
+ • \`unset <provider>\` – Remove credential env binding
23
+ • \`risk <command>\` – Diagnose risk for a shell command (e.g. exec)
24
+ • \`risk-patterns\` – List built-in dangerous commands and sensitive paths
25
+ • \`approve <approval_id>\` – Approve a pending high-risk tool call
26
+ • \`reject <approval_id>\` – Reject a pending high-risk tool call
27
+
28
+ Fetch: flow auto-inferred from control-plane provider type; override with \`--flow=oauth2-user|oauth2-m2m|apikey\`.
29
+
30
+ Examples:
31
+ \`/identity\` – show this help
32
+ \`/identity whoami\` – brief identity
33
+ \`/identity status\` – full status with TIP details
34
+ \`/identity login\`
35
+ \`/identity fetch google\`
36
+ \`/identity fetch openai --flow=apikey\`
37
+ \`/identity list 2\` – load more (page 2)
38
+ \`/identity risk "rm -rf /"\` – check if command would require approval
39
+ \`/identity risk-patterns\` – list risky patterns
40
+ \`/identity approve abc123\` – approve pending tool call
41
+ \`/identity reject abc123\` – reject pending tool call
42
+ \`/identity set google GOOGLE_ACCESS_TOKEN\`
43
+ \`/identity unset google\``;
44
+ function formatTtl(ms) {
45
+ const s = Math.floor(ms / 1000);
46
+ if (s >= 3600)
47
+ return `${Math.floor(s / 3600)}h${Math.floor((s % 3600) / 60)}m`;
48
+ if (s >= 60)
49
+ return `${Math.floor(s / 60)}m${s % 60}s`;
50
+ return `${s}s`;
51
+ }
52
+ /** Wrap URL as markdown link so Feishu and other platforms render it as clickable. */
53
+ function asMarkdownLink(url, label) {
54
+ return `[${label}](${url})`;
55
+ }
56
+ function formatAgo(ms) {
57
+ const s = Math.floor(ms / 1000);
58
+ if (s >= 3600)
59
+ return `${Math.floor(s / 3600)}h${Math.floor((s % 3600) / 60)}m`;
60
+ if (s >= 60)
61
+ return `${Math.floor(s / 60)}m`;
62
+ return `${s}s`;
63
+ }
64
+ function formatTimestamp(ms) {
65
+ const d = new Date(ms);
66
+ return d.toISOString().replace("T", " ").slice(0, 19);
67
+ }
68
+ function parseSubcommand(args) {
69
+ const raw = (args ?? "").trim();
70
+ const space = raw.indexOf(" ");
71
+ if (space === -1)
72
+ return { sub: raw.toLowerCase(), rest: "" };
73
+ return {
74
+ sub: raw.slice(0, space).toLowerCase(),
75
+ rest: raw.slice(space + 1).trim(),
76
+ };
77
+ }
78
+ /** Parse list args: optional page. E.g. "list", "list 2", "list --page 2". */
79
+ function parseListArgs(rest) {
80
+ const parts = rest.split(/\s+/).filter(Boolean);
81
+ const flags = {};
82
+ const positional = [];
83
+ for (const p of parts) {
84
+ if (p.startsWith("--")) {
85
+ const eq = p.indexOf("=");
86
+ if (eq > 2)
87
+ flags[p.slice(2, eq)] = p.slice(eq + 1);
88
+ }
89
+ else if (/^\d+$/.test(p)) {
90
+ positional.push(parseInt(p, 10));
91
+ }
92
+ }
93
+ const page = flags.page ? parseInt(flags.page, 10) : positional[0];
94
+ return { page: Number.isFinite(page) && page >= 1 ? page : 1 };
95
+ }
96
+ /** Parse fetch args: provider and flags (--flow, --redirectUrl, --scopes). flow can be omitted to auto-infer from provider. */
97
+ function parseFetchArgs(rest) {
98
+ const parts = rest.split(/\s+/).filter(Boolean);
99
+ const positional = [];
100
+ const flags = {};
101
+ for (const p of parts) {
102
+ if (p.startsWith("--")) {
103
+ const eq = p.indexOf("=");
104
+ if (eq > 2) {
105
+ flags[p.slice(2, eq)] = p.slice(eq + 1);
106
+ }
107
+ }
108
+ else {
109
+ positional.push(p);
110
+ }
111
+ }
112
+ const provider = positional[0]?.trim();
113
+ if (!provider) {
114
+ return {
115
+ error: "Usage: `/identity fetch <provider> [--flow=oauth2-user|oauth2-m2m|apikey] [--redirectUrl=<url>] [--scopes=a,b,c]`",
116
+ };
117
+ }
118
+ const flowRaw = flags.flow;
119
+ const flow = flowRaw === "oauth2-m2m"
120
+ ? "oauth2-m2m"
121
+ : flowRaw === "apikey"
122
+ ? "apikey"
123
+ : flowRaw === "oauth2-user"
124
+ ? "oauth2-user"
125
+ : undefined;
126
+ const redirectUrl = flags.redirectUrl?.trim() || undefined;
127
+ const scopes = flags.scopes
128
+ ? flags.scopes
129
+ .split(",")
130
+ .map((s) => s.trim())
131
+ .filter(Boolean)
132
+ : undefined;
133
+ return {
134
+ provider,
135
+ flow: flow ?? "oauth2-user",
136
+ flowExplicit: flowRaw != null,
137
+ redirectUrl: redirectUrl || undefined,
138
+ scopes,
139
+ };
140
+ }
141
+ function createIdentityHandler(deps) {
142
+ const { logger } = deps;
143
+ return async (ctx) => {
144
+ const { sub, rest } = parseSubcommand(ctx.args);
145
+ logger?.debug?.(`[identity] sub=${sub} rest=${rest.slice(0, 40)}...`);
146
+ const sessionKey = deriveSessionKey({
147
+ channel: ctx.channel,
148
+ senderId: ctx.senderId,
149
+ from: ctx.from,
150
+ to: ctx.to,
151
+ accountId: ctx.accountId,
152
+ config: ctx.config,
153
+ });
154
+ const needsSession = [
155
+ "login",
156
+ "logout",
157
+ "status",
158
+ "whoami",
159
+ "fetch",
160
+ "list-credentials",
161
+ "list",
162
+ "set",
163
+ "unset",
164
+ "approve",
165
+ "reject",
166
+ ].includes(sub);
167
+ if (needsSession && !sessionKey) {
168
+ return {
169
+ text: "⚠️ /identity requires session context (channel + sender). Use it from an active chat.",
170
+ };
171
+ }
172
+ switch (sub) {
173
+ case "help":
174
+ case "":
175
+ return { text: HELP_TEXT };
176
+ case "whoami":
177
+ return handleWhoami(ctx, deps, sessionKey);
178
+ case "login":
179
+ return handleLogin(ctx, deps, sessionKey);
180
+ case "status":
181
+ return handleStatus(ctx, deps, sessionKey);
182
+ case "logout":
183
+ return handleLogout(deps, sessionKey);
184
+ case "list-credentials":
185
+ case "list":
186
+ return handleListCredentials(deps, sessionKey, rest);
187
+ case "fetch":
188
+ return handleFetch(ctx, deps, sessionKey, rest);
189
+ case "set":
190
+ return handleSet(deps, sessionKey, rest);
191
+ case "unset":
192
+ return handleUnset(deps, sessionKey, rest);
193
+ case "list-tips":
194
+ case "tips":
195
+ return handleListTips(deps);
196
+ case "config":
197
+ return handleConfig(deps);
198
+ case "risk":
199
+ return handleRisk(deps, rest);
200
+ case "risk-patterns":
201
+ return handleRiskPatterns();
202
+ case "approve":
203
+ return handleApprove(deps, sessionKey, rest);
204
+ case "reject":
205
+ return handleReject(deps, sessionKey, rest);
206
+ default:
207
+ return {
208
+ text: `Unknown subcommand: \`${sub}\`. Use \`/identity help\` for usage.`,
209
+ };
210
+ }
211
+ };
212
+ }
213
+ async function handleWhoami(ctx, deps, sessionKey) {
214
+ const result = await runStatus(deps, sessionKey, ctx.config);
215
+ if (!result.loggedIn) {
216
+ return { text: "⚠ Not logged in. Run \`/identity login\`." };
217
+ }
218
+ const lines = [
219
+ `✓ Logged in as \`${result.sub}\``,
220
+ result.sessionLoginAt != null ? `Login: ${formatTimestamp(result.sessionLoginAt)}` : "",
221
+ result.hasTip
222
+ ? result.tipExpiresAt != null
223
+ ? `TIP: valid · expires ${formatTtl(result.tipExpiresAt - Date.now())}`
224
+ : "TIP: valid"
225
+ : "TIP: not refreshed (run \`/identity login\`)",
226
+ ];
227
+ return { text: lines.filter(Boolean).join("\n") };
228
+ }
229
+ async function handleLogin(ctx, deps, sessionKey) {
230
+ const deliveryTarget = deriveDeliveryTargetFromContext(ctx);
231
+ const result = await runLogin(deps, sessionKey, {
232
+ config: ctx.config,
233
+ deliveryTarget,
234
+ });
235
+ if (result.kind === "already_logged_in") {
236
+ return { text: `✓ Already logged in as ${result.sub}. TIP refreshed.` };
237
+ }
238
+ if (result.kind === "auth_url") {
239
+ return {
240
+ text: `Open this URL to log in:\n\n${asMarkdownLink(result.authUrl, "Click to log in")}\n\nAfter authorization, you can close the page. A success message will be sent to this chat.`,
241
+ };
242
+ }
243
+ return { text: `⚠️ OIDC login failed: ${result.message}` };
244
+ }
245
+ async function handleStatus(ctx, deps, sessionKey) {
246
+ const result = await runStatus(deps, sessionKey, ctx.config);
247
+ const lines = [];
248
+ const now = Date.now();
249
+ if (result.loggedIn) {
250
+ lines.push(`✓ Logged in as \`${result.sub}\``);
251
+ lines.push("");
252
+ lines.push("**Session**");
253
+ if (result.sessionLoginAt != null) {
254
+ lines.push(`• Login: ${formatTimestamp(result.sessionLoginAt)}`);
255
+ }
256
+ if (result.sessionExpiresAt != null) {
257
+ const diff = result.sessionExpiresAt - now;
258
+ lines.push(diff > 0
259
+ ? `• Expires: ${formatTimestamp(result.sessionExpiresAt)} (in ${formatTtl(diff)})`
260
+ : `• Expired: ${formatTimestamp(result.sessionExpiresAt)}`);
261
+ }
262
+ lines.push("");
263
+ lines.push("**TIP Token**");
264
+ if (result.hasTip) {
265
+ if (result.tipIssuedAt != null) {
266
+ lines.push(`• Issued: ${formatTimestamp(result.tipIssuedAt)}`);
267
+ }
268
+ if (result.tipExpiresAt != null) {
269
+ const diff = result.tipExpiresAt - now;
270
+ lines.push(diff > 0
271
+ ? `• Expires: ${formatTimestamp(result.tipExpiresAt)} (in ${formatTtl(diff)})`
272
+ : `• Expired: ${formatTimestamp(result.tipExpiresAt)}`);
273
+ }
274
+ if (result.tipChain && result.tipChain.length > 0) {
275
+ lines.push(`• Chain: ${result.tipChain.join(" → ")}`);
276
+ }
277
+ }
278
+ else {
279
+ lines.push("• Status: ⚠ not refreshed (run \`/identity login\`)");
280
+ }
281
+ }
282
+ else {
283
+ lines.push("⚠ Not logged in. Run \`/identity login\`.");
284
+ }
285
+ const providers = Object.keys(result.credentials);
286
+ if (providers.length > 0) {
287
+ lines.push("");
288
+ lines.push("**Credentials:**");
289
+ for (const p of providers) {
290
+ const c = result.credentials[p];
291
+ const bind = result.bindings[p] ? ` → \`${result.bindings[p]}\`` : "";
292
+ if (c.type === "oauth2") {
293
+ const exp = c.expiresAt != null
294
+ ? c.expiresAt < now
295
+ ? `expired ${formatAgo(now - c.expiresAt)} ago`
296
+ : `expires in ${formatTtl(c.expiresAt - now)}`
297
+ : "";
298
+ const refresh = c.refreshToken ? ", has refresh" : "";
299
+ const scopes = c.scopes?.length ? ` [${c.scopes.join(", ")}]` : "";
300
+ lines.push(`• **${p}** (oauth2): ${c.status}${exp}${refresh}${scopes}${bind}`);
301
+ }
302
+ else if (c.type === "api_key") {
303
+ const src = c.valueFromEnv ? `from \`${c.valueFromEnv}\`` : c.value ? "stored" : "empty";
304
+ lines.push(`• **${p}** (api_key): key=\`${c.key}\`, ${src}${bind}`);
305
+ }
306
+ else {
307
+ lines.push(`• **${p}**: ?${bind}`);
308
+ }
309
+ }
310
+ }
311
+ return { text: lines.join("\n") || "No status." };
312
+ }
313
+ async function handleLogout(deps, sessionKey) {
314
+ await runLogout(deps, sessionKey);
315
+ return { text: "✓ Logged out." };
316
+ }
317
+ async function handleListCredentials(deps, sessionKey, rest) {
318
+ const { page } = parseListArgs(rest);
319
+ const result = await runListCredentials(deps, sessionKey, page);
320
+ const lines = [];
321
+ if (result.providers.length > 0) {
322
+ lines.push(`**Available (page ${result.page}):**`);
323
+ for (const p of result.providers) {
324
+ const bind = p.binding ? ` → \`${p.binding}\`` : "";
325
+ lines.push(`• **${p.name}** (${p.type}): ${p.status}${bind}`);
326
+ }
327
+ }
328
+ if (result.storedOnly.length > 0) {
329
+ if (lines.length)
330
+ lines.push("");
331
+ lines.push("**Your credentials:**");
332
+ for (const item of result.storedOnly) {
333
+ const bind = item.binding ? ` → \`${item.binding}\`` : "";
334
+ lines.push(`• **${item.name}**: ${item.status}${bind}`);
335
+ }
336
+ }
337
+ if (lines.length === 0) {
338
+ return {
339
+ text: "No providers. Configure credential providers in the control plane, or use `/identity fetch <provider>` to add.",
340
+ };
341
+ }
342
+ const footer = [];
343
+ footer.push("Use `/identity set <provider> <envVar>` to bind.");
344
+ if (result.hasMore) {
345
+ footer.push(`\`/identity list ${result.page + 1}\` to load more.`);
346
+ }
347
+ return {
348
+ text: `${lines.join("\n")}\n\n${footer.join(" ")}`,
349
+ };
350
+ }
351
+ async function handleFetch(ctx, deps, sessionKey, rest) {
352
+ const parsed = parseFetchArgs(rest);
353
+ if ("error" in parsed) {
354
+ return { text: parsed.error };
355
+ }
356
+ const { provider, flow, flowExplicit, redirectUrl, scopes } = parsed;
357
+ const deliveryTarget = deriveDeliveryTargetFromContext(ctx);
358
+ const result = await runFetch(deps, sessionKey, {
359
+ provider,
360
+ flow,
361
+ flowExplicit,
362
+ redirectUrl,
363
+ scopes,
364
+ deliveryTarget,
365
+ config: ctx.config,
366
+ });
367
+ if (result.kind === "success")
368
+ return { text: result.message };
369
+ if (result.kind === "auth_url") {
370
+ return {
371
+ text: `Open this URL to authorize \`${provider}\`:\n\n${asMarkdownLink(result.authUrl, "Click to authorize")}\n\nAfter authorization, a success message will be sent here.`,
372
+ };
373
+ }
374
+ return { text: `⚠️ ${result.message}` };
375
+ }
376
+ async function handleSet(deps, sessionKey, rest) {
377
+ const parts = rest.split(/\s+/).filter(Boolean);
378
+ if (parts.length < 2) {
379
+ return {
380
+ text: "Usage: `/identity set <provider> <envVar>`\nExample: `/identity set google GOOGLE_ACCESS_TOKEN`\n\n• If credential exists: binds it for injection as env var when tools run.\n• If not: imports from process.env[envVar] as api_key (gateway must have that env set).",
381
+ };
382
+ }
383
+ const provider = parts[0];
384
+ const envVar = parts[1];
385
+ const result = await runSetBinding(deps, sessionKey, { provider, envVar });
386
+ if (result.ok)
387
+ return { text: result.message ?? "✓ Bound." };
388
+ return { text: `⚠️ ${result.error}` };
389
+ }
390
+ async function handleConfig(deps) {
391
+ const result = await runConfig(deps);
392
+ if (Object.keys(result).length === 0) {
393
+ return { text: "No identity plugin config loaded." };
394
+ }
395
+ const lines = ["**Identity plugin config:**"];
396
+ const id = result.identity;
397
+ if (id && Object.keys(id).length > 0) {
398
+ lines.push("");
399
+ lines.push("**identity:**");
400
+ for (const [k, v] of Object.entries(id)) {
401
+ if (v != null)
402
+ lines.push(`• ${k}: \`${v}\``);
403
+ }
404
+ }
405
+ else {
406
+ lines.push("");
407
+ lines.push("**identity:** not configured");
408
+ }
409
+ const up = result.userpool;
410
+ if (up && Object.keys(up).length > 0) {
411
+ lines.push("");
412
+ lines.push("**userpool:**");
413
+ for (const [k, v] of Object.entries(up)) {
414
+ if (v != null)
415
+ lines.push(`• ${k}: \`${v}\``);
416
+ }
417
+ }
418
+ else {
419
+ lines.push("");
420
+ lines.push("**userpool:** not configured");
421
+ }
422
+ const authz = result.authz;
423
+ if (authz && Object.keys(authz).length > 0) {
424
+ lines.push("");
425
+ lines.push("**authz:**");
426
+ for (const [k, v] of Object.entries(authz)) {
427
+ if (v != null)
428
+ lines.push(`• ${k}: ${v}`);
429
+ }
430
+ }
431
+ return { text: lines.join("\n") };
432
+ }
433
+ async function handleRisk(deps, rest) {
434
+ const command = rest.trim();
435
+ if (!command) {
436
+ return {
437
+ text: "Usage: `/identity risk <command>`\nExample: `/identity risk \"rm -rf /\"`\n\nDiagnoses risk for a shell command (as if run via exec). Use tools for tool-level checks.",
438
+ };
439
+ }
440
+ const llmConfig = deps.pluginConfig?.authz?.enableLlmRiskCheck && deps.pluginConfig?.authz?.llmRiskCheck
441
+ ? deps.pluginConfig.authz.llmRiskCheck
442
+ : undefined;
443
+ const result = await diagnoseRisk("exec", { command }, llmConfig, deps.logger);
444
+ const riskEmoji = result.risk === "high" ? "🔴" : result.risk === "medium" ? "🟡" : "🟢";
445
+ const lines = [
446
+ `**Risk diagnosis** (${result.source}): ${riskEmoji} **${result.risk}**`,
447
+ `Command: \`${command.slice(0, 120)}${command.length > 120 ? "…" : ""}\``,
448
+ ];
449
+ if (result.reason)
450
+ lines.push(`Reason: ${result.reason}`);
451
+ return { text: lines.join("\n") };
452
+ }
453
+ async function handleApprove(deps, sessionKey, rest) {
454
+ const approvalId = rest.trim();
455
+ if (!approvalId) {
456
+ return {
457
+ text: "Usage: `/identity approve <approval_id>`\nExample: `/identity approve abc123`\n\nUse the approval_id from the pending tool message. Must run from the same chat that triggered the approval request.",
458
+ };
459
+ }
460
+ const approvalTtlMs = (deps.pluginConfig?.authz?.approvalTtlSeconds ?? 300) * 1000;
461
+ const ok = toolApprovalStore.approve(approvalId, approvalTtlMs, sessionKey);
462
+ if (ok)
463
+ return { text: "✓ Tool call approved. You can retry the action now." };
464
+ return {
465
+ text: "⚠ Approval not found or expired. Run from the same chat that requested approval, or the request may have timed out.",
466
+ };
467
+ }
468
+ async function handleReject(deps, sessionKey, rest) {
469
+ const approvalId = rest.trim();
470
+ if (!approvalId) {
471
+ return {
472
+ text: "Usage: `/identity reject <approval_id>`\nExample: `/identity reject abc123`\n\nMust run from the same chat that triggered the approval request.",
473
+ };
474
+ }
475
+ const ok = toolApprovalStore.reject(approvalId, sessionKey);
476
+ if (ok)
477
+ return { text: "✓ Tool call rejected." };
478
+ return { text: "⚠ Approval not found or already expired. Run from the same chat that requested approval." };
479
+ }
480
+ async function handleRiskPatterns() {
481
+ const { commandPatterns, sensitivePaths } = getRiskPatterns();
482
+ const lines = [
483
+ "**Built-in risk patterns** (exec/process):",
484
+ "",
485
+ ...commandPatterns.map((p) => `• \`${p.example}\` – ${p.reason}`),
486
+ "",
487
+ "**Sensitive paths** (write/edit/apply_patch):",
488
+ "",
489
+ ...sensitivePaths.map((p) => `• \`${p}\``),
490
+ "",
491
+ "Commands or writes matching these require approval when authz is enabled.",
492
+ ];
493
+ return { text: lines.join("\n") };
494
+ }
495
+ async function handleUnset(deps, sessionKey, rest) {
496
+ const provider = rest.split(/\s+/)[0]?.trim();
497
+ if (!provider) {
498
+ return {
499
+ text: "Usage: `/identity unset <provider>`\nExample: `/identity unset google`",
500
+ };
501
+ }
502
+ const result = await runUnsetBinding(deps, sessionKey, { provider });
503
+ if (result.ok)
504
+ return { text: result.message ?? "✓ Unset." };
505
+ return { text: result.error ?? "No binding found." };
506
+ }
507
+ function formatBindings(bindings) {
508
+ if (Object.keys(bindings).length === 0)
509
+ return "None.";
510
+ return Object.entries(bindings)
511
+ .map(([p, v]) => `• ${p} → \`${v}\``)
512
+ .join("\n");
513
+ }
514
+ async function handleListTips(deps) {
515
+ const result = await runListTips(deps);
516
+ const { bindingsBySession } = result;
517
+ if (result.tips.length === 0) {
518
+ return {
519
+ text: "No valid TIP tokens. Run `/identity login` to acquire one.",
520
+ };
521
+ }
522
+ const lines = ["**Valid TIP tokens:**"];
523
+ for (const v of result.tips) {
524
+ const shortKey = v.sessionKey.length > 40 ? v.sessionKey.slice(0, 36) + "…" : v.sessionKey;
525
+ const chainStr = v.chain.length > 0 ? ` (${v.chain.join(" → ")})` : "";
526
+ const expiresIn = v.ttlSec >= 3600
527
+ ? `${Math.floor(v.ttlSec / 3600)}h${Math.floor((v.ttlSec % 3600) / 60)}m`
528
+ : `${v.ttlSec}s`;
529
+ lines.push(`• \`${shortKey}\` | sub: \`${v.sub}\`${chainStr} | expires in ${expiresIn}`);
530
+ }
531
+ lines.push("");
532
+ lines.push("**Env bindings (per session):**");
533
+ const allSessionKeys = new Set([
534
+ ...result.tips.map((t) => t.sessionKey),
535
+ ...Object.keys(bindingsBySession),
536
+ ]);
537
+ if (allSessionKeys.size === 0) {
538
+ lines.push("None.");
539
+ }
540
+ else {
541
+ for (const sk of [...allSessionKeys].sort()) {
542
+ const b = bindingsBySession[sk] ?? {};
543
+ if (Object.keys(b).length === 0)
544
+ continue;
545
+ const shortKey = sk.length > 40 ? sk.slice(0, 36) + "…" : sk;
546
+ lines.push(`\`${shortKey}\`:`);
547
+ lines.push(formatBindings(b));
548
+ }
549
+ }
550
+ return { text: lines.join("\n") };
551
+ }
552
+ export function createIdentityCommand(deps) {
553
+ const handler = createIdentityHandler(deps);
554
+ return {
555
+ name: "identity",
556
+ description: "UserPool login, TIP token, credentials (list/fetch/set), risk check",
557
+ acceptsArgs: true,
558
+ requireAuth: true,
559
+ handler,
560
+ };
561
+ }
562
+ /** Alias command /id same as /identity */
563
+ export function createIdCommand(deps) {
564
+ const handler = createIdentityHandler(deps);
565
+ return {
566
+ name: "id",
567
+ description: "Alias for /identity (login, status, credentials, risk check)",
568
+ acceptsArgs: true,
569
+ requireAuth: true,
570
+ handler,
571
+ };
572
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * after_tool_call hook: propagate TIP token to child session on sessions_spawn.
3
+ * When parent spawns a sub-agent, inherit TIP with extended chain-of-custody.
4
+ */
5
+ export type AfterToolCallDeps = {
6
+ storeDir: string;
7
+ logger: {
8
+ info?: (msg: string) => void;
9
+ };
10
+ };
11
+ export declare function createAfterToolCallHandler(deps: AfterToolCallDeps): (event: {
12
+ toolName: string;
13
+ params: Record<string, unknown>;
14
+ result?: unknown;
15
+ error?: string;
16
+ durationMs?: number;
17
+ }, ctx: {
18
+ agentId?: string;
19
+ sessionKey?: string;
20
+ toolName: string;
21
+ }) => Promise<void>;
22
+ //# sourceMappingURL=after-tool-call.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"after-tool-call.d.ts","sourceRoot":"","sources":["../../../src/hooks/after-tool-call.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE;QAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAC1C,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,iBAAiB,IAI9D,OAAO;IACL,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,EACD,KAAK;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,KAC/D,OAAO,CAAC,IAAI,CAAC,CA4BjB"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * after_tool_call hook: propagate TIP token to child session on sessions_spawn.
3
+ * When parent spawns a sub-agent, inherit TIP with extended chain-of-custody.
4
+ */
5
+ import { getTIPToken, setTIPToken } from "../store/tip-store.js";
6
+ export function createAfterToolCallHandler(deps) {
7
+ const { storeDir, logger } = deps;
8
+ return async (event, ctx) => {
9
+ if (event.toolName !== "sessions_spawn")
10
+ return;
11
+ const result = event.result;
12
+ const childSessionKey = result?.childSessionKey;
13
+ if (!childSessionKey || typeof childSessionKey !== "string")
14
+ return;
15
+ const parentSessionKey = ctx.sessionKey;
16
+ if (!parentSessionKey)
17
+ return;
18
+ try {
19
+ const parentTIP = await getTIPToken(storeDir, parentSessionKey);
20
+ if (!parentTIP)
21
+ return;
22
+ const childTIP = {
23
+ ...parentTIP,
24
+ chain: [...(parentTIP.chain ?? []), ctx.agentId ?? "unknown"],
25
+ parentSessionKey,
26
+ issuedAt: Date.now(),
27
+ };
28
+ await setTIPToken(storeDir, childSessionKey, childTIP);
29
+ logger.info?.(`agent-identity: TIP propagated to child ${childSessionKey.slice(0, 32)}...`);
30
+ }
31
+ catch (err) {
32
+ logger.info?.(`agent-identity: failed to propagate TIP to child: ${String(err)}`);
33
+ }
34
+ };
35
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * before_agent_start hook: fetch TIP token and inject credentials into process.env.
3
+ * 1. Inject credentials into process.env per credential-env-bindings (per-session)
4
+ * 2. Look up userToken from session store by sessionKey
5
+ * 3. Call CIS GetWorkloadAccessTokenForJWT
6
+ * 4. On "token has expired", refresh userToken via refresh_token grant and retry
7
+ * 5. Store TIP token in tip-store for use by before_tool_call (AuthZ) and downstream
8
+ */
9
+ import type { IdentityService } from "../services/identity-service.js";
10
+ import type { OIDCConfigForRefresh } from "../services/session-refresh.js";
11
+ export type BeforeAgentStartDeps = {
12
+ storeDir: string;
13
+ identityService: IdentityService;
14
+ /** When set, used to refresh userToken on expiry before retrying TIP fetch. */
15
+ getOidcConfigForRefresh?: () => Promise<OIDCConfigForRefresh>;
16
+ logger: {
17
+ info?: (msg: string) => void;
18
+ warn?: (msg: string) => void;
19
+ };
20
+ };
21
+ export declare function createBeforeAgentStartHandler(deps: BeforeAgentStartDeps): (_event: {
22
+ prompt: string;
23
+ messages?: unknown[];
24
+ }, ctx: {
25
+ agentId?: string;
26
+ sessionKey?: string;
27
+ }) => Promise<{
28
+ prependContext?: string;
29
+ } | void>;
30
+ //# sourceMappingURL=before-agent-start.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"before-agent-start.d.ts","sourceRoot":"","sources":["../../../src/hooks/before-agent-start.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAQ3E,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,eAAe,CAAC;IACjC,+EAA+E;IAC/E,uBAAuB,CAAC,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC9D,MAAM,EAAE;QAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CACxE,CAAC;AAOF,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,oBAAoB,IAIpE,QAAQ;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,EAChD,KAAK;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,KAC7C,OAAO,CAAC;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAgF/C"}