@m1a0rz/agent-identity 0.3.1 → 0.3.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 (47) hide show
  1. package/README-cn.md +19 -9
  2. package/README.md +22 -12
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +18 -17
  5. package/dist/src/actions/identity-actions.d.ts +7 -1
  6. package/dist/src/actions/identity-actions.d.ts.map +1 -1
  7. package/dist/src/actions/identity-actions.js +82 -34
  8. package/dist/src/commands/identity-commands.d.ts.map +1 -1
  9. package/dist/src/commands/identity-commands.js +9 -4
  10. package/dist/src/hooks/after-tool-call.d.ts +12 -0
  11. package/dist/src/hooks/after-tool-call.d.ts.map +1 -0
  12. package/dist/src/hooks/after-tool-call.js +31 -0
  13. package/dist/src/hooks/before-agent-start.d.ts +3 -3
  14. package/dist/src/hooks/before-agent-start.d.ts.map +1 -1
  15. package/dist/src/hooks/before-agent-start.js +6 -20
  16. package/dist/src/hooks/before-tool-call.d.ts +19 -14
  17. package/dist/src/hooks/before-tool-call.d.ts.map +1 -1
  18. package/dist/src/hooks/before-tool-call.js +192 -130
  19. package/dist/src/hooks/subagent-ended-cleanup.js +1 -1
  20. package/dist/src/risk/low-risk-tools.d.ts.map +1 -1
  21. package/dist/src/risk/low-risk-tools.js +0 -2
  22. package/dist/src/services/tip-acquisition.d.ts +0 -1
  23. package/dist/src/services/tip-acquisition.d.ts.map +1 -1
  24. package/dist/src/services/tip-acquisition.js +2 -2
  25. package/dist/src/services/tip-propagation.d.ts.map +1 -1
  26. package/dist/src/services/tip-propagation.js +1 -2
  27. package/dist/src/services/tip-with-refresh.d.ts +1 -1
  28. package/dist/src/services/tip-with-refresh.d.ts.map +1 -1
  29. package/dist/src/services/tip-with-refresh.js +3 -5
  30. package/dist/src/store/credential-env-snapshot.d.ts +38 -0
  31. package/dist/src/store/credential-env-snapshot.d.ts.map +1 -0
  32. package/dist/src/store/credential-env-snapshot.js +101 -0
  33. package/dist/src/store/encryption.d.ts +30 -0
  34. package/dist/src/store/encryption.d.ts.map +1 -0
  35. package/dist/src/store/encryption.js +147 -0
  36. package/dist/src/store/session-store.d.ts.map +1 -1
  37. package/dist/src/store/session-store.js +91 -8
  38. package/dist/src/store/tip-store.d.ts +11 -6
  39. package/dist/src/store/tip-store.d.ts.map +1 -1
  40. package/dist/src/store/tip-store.js +23 -54
  41. package/dist/src/tools/identity-fetch.d.ts.map +1 -1
  42. package/dist/src/tools/identity-fetch.js +1 -0
  43. package/dist/src/tools/identity-list-credentials.d.ts +2 -0
  44. package/dist/src/tools/identity-list-credentials.d.ts.map +1 -1
  45. package/dist/src/tools/identity-list-credentials.js +6 -3
  46. package/package.json +1 -1
  47. package/skills/SKILL.md +75 -150
@@ -1 +1 @@
1
- {"version":3,"file":"before-tool-call.d.ts","sourceRoot":"","sources":["../../../src/hooks/before-tool-call.ts"],"names":[],"mappings":"AAgBA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAYhD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,uGAAuG;IACvG,cAAc,CAAC,EAAE,uBAAuB,CAAC;IACzC,kFAAkF;IAClF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;IACxE,sEAAsE;IACtE,aAAa,CAAC,EAAE,CAAC,kBAAkB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,oBAAoB;IACpB,KAAK,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,uBAAuB,CAAC,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC9D,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAwCF,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,kBAAkB,IA8BhE,OAAO;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAC5E,KAAK;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,KAC/D,OAAO,CAAC;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA2L7D"}
1
+ {"version":3,"file":"before-tool-call.d.ts","sourceRoot":"","sources":["../../../src/hooks/before-tool-call.ts"],"names":[],"mappings":"AAgBA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAiBhD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,uBAAuB,CAAC;IACzC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;IACxE,aAAa,CAAC,EAAE,CAAC,kBAAkB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,KAAK,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,uBAAuB,CAAC,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC9D,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAoCF,iBAAe,kBAAkB,CAC/B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CASjC;AAkCD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,kBAAkB,IAsBhE,OAAO;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EACjG,KAAK;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,KAC/D,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA+F9B;AAwJD,2BAA2B;AAC3B,eAAO,MAAM,SAAS;;CAAyB,CAAC"}
@@ -17,57 +17,88 @@ import { diagnoseRisk } from "../risk/diagnose-risk.js";
17
17
  import { isLowRiskTool } from "../risk/low-risk-tools.js";
18
18
  import * as skillPathStore from "../store/skill-path-store.js";
19
19
  import * as toolApprovalStore from "../store/tool-approval-store.js";
20
+ import { loadCredentialEnvBindings } from "../store/credential-env-bindings.js";
21
+ import { getCredential, resolveCredentialValue } from "../store/credential-store.js";
22
+ import { applyEnvSnapshot } from "../store/credential-env-snapshot.js";
20
23
  import { getOrRefreshTIPToken } from "../services/tip-with-refresh.js";
21
24
  import { supportsSyncApproval } from "../utils/approval-channel.js";
22
25
  import { extractDelegationChainFromJwt } from "../utils/auth.js";
23
- import { logDebug } from "../utils/logger.js";
26
+ import { isSubagentSessionKey, isGroupOrChannelSessionKey } from "../utils/derive-session-key.js";
27
+ import { LOG_PREFIX, logDebug, logWarn } from "../utils/logger.js";
24
28
  import { getSender, resolveEffectiveSessionKeyForRun } from "../store/group-sender-store.js";
25
- import { isGroupOrChannelSessionKey } from "../utils/derive-session-key.js";
29
+ // ─── Exempt tools (bypass session + authz entirely) ──────────────────
30
+ const IDENTITY_EXEMPT_TOOLS = new Set([
31
+ "identity_login",
32
+ "identity_logout",
33
+ "identity_whoami",
34
+ "identity_status",
35
+ "identity_config_suggest",
36
+ ]);
37
+ /** Tools and reads that are exempt from both session validation and authz. */
38
+ function isSessionExempt(toolName, params, sessionKey) {
39
+ if (IDENTITY_EXEMPT_TOOLS.has(toolName.trim().toLowerCase()))
40
+ return true;
41
+ // Identity plugin's own SKILL.md must always be loadable
42
+ const pathStr = params?.path ?? params?.file_path;
43
+ return (toolName.toLowerCase() === "read" &&
44
+ isSkillReadPath(pathStr) &&
45
+ skillPathStore.getSkillNameForPath(sessionKey, String(pathStr ?? "")) === "identity");
46
+ }
47
+ function isSkillReadPath(pathStr) {
48
+ if (typeof pathStr !== "string")
49
+ return false;
50
+ const p = pathStr.trim().replace(/\\/g, "/");
51
+ return p.endsWith("SKILL.md") || p.endsWith("/SKILL.md");
52
+ }
53
+ // ─── Helpers ─────────────────────────────────────────────────────────
54
+ async function resolveCredentials(storeDir, effectiveKey) {
55
+ const bindings = await loadCredentialEnvBindings(storeDir, effectiveKey);
56
+ const values = {};
57
+ for (const [provider, envVar] of Object.entries(bindings)) {
58
+ const cred = await getCredential(storeDir, effectiveKey, provider);
59
+ const value = cred ? resolveCredentialValue(cred) : undefined;
60
+ if (value)
61
+ values[envVar] = value;
62
+ }
63
+ return values;
64
+ }
26
65
  function resolveAuthzFlags(authz) {
66
+ const toolCheck = authz?.toolCheck ?? false;
67
+ const skillReadCheck = authz?.skillReadCheck ?? false;
68
+ const requireRiskApproval = authz?.requireRiskApproval ?? false;
69
+ const enableLlmRiskCheck = authz?.enableLlmRiskCheck ?? false;
27
70
  return {
28
- toolCheck: authz?.toolCheck ?? false,
29
- skillReadCheck: authz?.skillReadCheck ?? false,
30
- requireRiskApproval: authz?.requireRiskApproval ?? false,
31
- enableLlmRiskCheck: authz?.enableLlmRiskCheck ?? false,
71
+ toolCheck,
72
+ skillReadCheck,
73
+ requireRiskApproval,
74
+ enableLlmRiskCheck,
75
+ hasAnyAuthz: toolCheck || skillReadCheck || requireRiskApproval,
32
76
  };
33
77
  }
34
78
  function buildApprovalMessage(toolName, params, approvalId, ttlSeconds, riskReason) {
35
79
  const preview = toolName === "exec" || toolName === "process"
36
80
  ? String(params.command ?? params.cmd ?? params.script ?? "").slice(0, 80)
37
81
  : String(params.path ?? params.target ?? "").slice(0, 80);
38
- const reasonLine = riskReason && riskReason.trim()
39
- ? `\nRisk: ${riskReason.trim()}`
40
- : "";
82
+ const reasonLine = riskReason && riskReason.trim() ? `\nRisk: ${riskReason.trim()}` : "";
41
83
  return `Tool "${toolName}"${preview ? ` (${preview}...)` : ""} requires your approval.${reasonLine}\nReply "approve" or /identity approve ${approvalId}. Expires in ${ttlSeconds}s.`;
42
84
  }
43
- function isSkillReadPath(pathStr) {
44
- if (typeof pathStr !== "string")
45
- return false;
46
- const p = pathStr.trim().replace(/\\/g, "/");
47
- return p.endsWith("SKILL.md") || p.endsWith("/SKILL.md");
48
- }
85
+ // ─── Main handler ────────────────────────────────────────────────────
49
86
  export function createBeforeToolCallHandler(deps) {
50
- const { storeDir, identityClient, namespaceName = "default", logger, sendToSession, authz, approvalTtlMs, identityService, getOidcConfigForRefresh, configWorkloadName, } = deps;
87
+ const { storeDir, identityClient, namespaceName = "default", logger, sendToSession, authz, approvalTtlMs = 300_000, identityService, getOidcConfigForRefresh, configWorkloadName, } = deps;
51
88
  const flags = resolveAuthzFlags(authz);
52
- // Pass identityService when available so we can try fetch with session.userToken when TIP is missing/expired.
53
- // getOidcConfigForRefresh is only needed for refresh when userToken itself has expired.
54
89
  const tipRefreshOptions = identityService
55
- ? {
56
- identityService,
57
- getOidcConfigForRefresh,
58
- configWorkloadName,
59
- logger,
60
- }
90
+ ? { identityService, getOidcConfigForRefresh, configWorkloadName, logger }
61
91
  : undefined;
62
92
  const lowRiskBypass = authz?.lowRiskBypass !== false;
63
93
  const extraLowRisk = authz?.lowRiskTools;
64
94
  return async (event, ctx) => {
65
95
  const { toolName, params } = event;
66
96
  const sessionKey = ctx.sessionKey;
67
- logDebug(logger, `before_tool_call toolName=${toolName}`);
97
+ logDebug(logger, `before_tool_call tool=${toolName} session=${sessionKey ?? "?"}`);
68
98
  if (!sessionKey)
69
99
  return;
70
100
  const effectiveKey = resolveEffectiveSessionKeyForRun(sessionKey, event.runId);
101
+ // Phase 1: group sender context
71
102
  if (isGroupOrChannelSessionKey(sessionKey)) {
72
103
  const groupSender = getSender(sessionKey);
73
104
  if (groupSender) {
@@ -83,127 +114,158 @@ export function createBeforeToolCallHandler(deps) {
83
114
  };
84
115
  }
85
116
  }
86
- const pathStr = params?.path ?? params?.file_path;
87
- const isSkillRead = flags.skillReadCheck &&
88
- toolName.toLowerCase() === "read" &&
89
- isSkillReadPath(pathStr);
90
- const skillName = isSkillRead
91
- ? skillPathStore.getSkillNameForPath(sessionKey, String(pathStr ?? ""))
92
- : undefined;
93
- const shouldRunCheckPermission = (flags.toolCheck && !isSkillRead) || (flags.skillReadCheck && isSkillRead && skillName != null);
94
- const shouldBypassLowRisk = lowRiskBypass &&
95
- isLowRiskTool(toolName, extraLowRisk) &&
96
- !(isSkillRead && skillName);
97
- if (shouldBypassLowRisk) {
98
- logDebug(logger, `low-risk bypass for ${toolName}`);
99
- return;
100
- }
101
- const tip = await getOrRefreshTIPToken(storeDir, effectiveKey, tipRefreshOptions
102
- ? { ...tipRefreshOptions, ctxAgentId: ctx.agentId }
103
- : undefined);
104
- if (!tip) {
105
- return {
106
- block: true,
107
- blockReason: "AuthZ: session has no valid identity (TIP token required)",
108
- };
109
- }
110
- if (shouldRunCheckPermission && identityClient) {
111
- const chain = extractDelegationChainFromJwt(tip.token);
112
- if (!chain) {
117
+ const exempt = isSessionExempt(toolName, params, sessionKey);
118
+ // Phase 2a: mandatory session validation (all non-exempt tools)
119
+ if (!exempt) {
120
+ const tip = await getOrRefreshTIPToken(storeDir, effectiveKey, tipRefreshOptions ? { ...tipRefreshOptions, ctxAgentId: ctx.agentId } : undefined);
121
+ if (!tip) {
122
+ logDebug(logger, `session: no TIP for tool=${toolName} key=${effectiveKey}`);
113
123
  return {
114
124
  block: true,
115
- blockReason: "AuthZ: failed to parse delegation chain from TIP token",
125
+ blockReason: `${LOG_PREFIX} session has no valid identity (TIP token required)`,
116
126
  };
117
127
  }
118
- const principal = { Type: "user", Id: chain.principalId };
119
- const action = { Type: "Action", Id: "invoke" };
120
- const resource = skillName != null
121
- ? { Type: "skill", Id: skillName }
122
- : { Type: "tool", Id: toolName };
123
- const originalCallers = chain.actors.slice().reverse().map((id) => ({
124
- Type: "agent",
125
- Id: id,
126
- }));
127
- logDebug(logger, `before_tool_call checking permission for ${resource.Type}:${resource.Id} (sub: ${tip.sub}), originalCallers: ${originalCallers.map((c) => c.Id).join(", ")}`);
128
- try {
129
- const result = await identityClient.checkPermission({
130
- namespaceName,
131
- principal,
132
- action,
133
- resource,
134
- originalCallers,
135
- });
136
- if (!result.allowed) {
137
- return {
138
- block: true,
139
- blockReason: result.message ||
140
- `AuthZ: CheckPermission denied for ${resource.Type} ${resource.Id}`,
141
- };
142
- }
143
- }
144
- catch (err) {
145
- logDebug(logger, `CheckPermission error: ${String(err)}`);
146
- return {
147
- block: true,
148
- blockReason: `AuthZ: Failed to verify permission: ${String(err)}`,
149
- };
150
- }
151
- if (skillName != null) {
152
- logDebug(logger, `skill read allowed for ${skillName}`);
153
- return;
128
+ }
129
+ // Phase 2b: permission + risk checks (when authz configured, skip exempt)
130
+ if (flags.hasAnyAuthz && !exempt) {
131
+ const authzBlock = await runPermissionAndRiskChecks({
132
+ toolName,
133
+ params,
134
+ sessionKey,
135
+ effectiveKey,
136
+ agentId: ctx.agentId,
137
+ flags,
138
+ storeDir,
139
+ identityClient,
140
+ namespaceName,
141
+ logger,
142
+ sendToSession,
143
+ authz,
144
+ approvalTtlMs,
145
+ tipRefreshOptions,
146
+ lowRiskBypass,
147
+ extraLowRisk,
148
+ });
149
+ if (authzBlock) {
150
+ logDebug(logger, `authz blocked tool=${toolName}: ${authzBlock.blockReason}`);
151
+ return authzBlock;
154
152
  }
155
153
  }
156
- if (!flags.requireRiskApproval) {
157
- logDebug(logger, `AuthZ ok for ${toolName} (sub: ${tip.sub})`);
154
+ // Phase 3: credential injection (non-subagent only)
155
+ if (isSubagentSessionKey(sessionKey))
158
156
  return;
157
+ let credentialValues;
158
+ try {
159
+ const creds = await resolveCredentials(storeDir, effectiveKey);
160
+ if (Object.keys(creds).length > 0) {
161
+ credentialValues = creds;
162
+ }
159
163
  }
160
- const paramsRecord = params && typeof params === "object" ? params : {};
161
- const llmConfig = flags.enableLlmRiskCheck && authz?.llmRiskCheck ? authz.llmRiskCheck : undefined;
162
- const { risk, reason: riskReason } = await diagnoseRisk(toolName, paramsRecord, llmConfig, logger);
163
- if (risk !== "high") {
164
- logDebug(logger, `AuthZ ok for ${toolName} (risk=${risk})`);
165
- return;
164
+ catch (err) {
165
+ logWarn(logger, `credential resolve failed for key=${effectiveKey}: ${String(err)}`);
166
166
  }
167
- if (toolApprovalStore.hasRecentApproval(effectiveKey, toolName, paramsRecord)) {
168
- toolApprovalStore.consumeApproval(effectiveKey, toolName, paramsRecord);
169
- logDebug(logger, `AuthZ ok for ${toolName} (recent approval)`);
167
+ if (!credentialValues)
170
168
  return;
169
+ const envKeys = Object.keys(credentialValues);
170
+ logDebug(logger, `injecting ${envKeys.length} credential(s) [${envKeys.join(", ")}] ` +
171
+ `for tool=${toolName} run=${event.runId ?? "?"} key=${effectiveKey}`);
172
+ await applyEnvSnapshot(credentialValues, event.runId, event.toolCallId);
173
+ return { params: { _credentialContext: { ...credentialValues } } };
174
+ };
175
+ }
176
+ async function runPermissionAndRiskChecks(c) {
177
+ const { toolName, params, sessionKey, effectiveKey, flags, logger } = c;
178
+ // Determine if this is a SKILL.md read that needs skill-level CheckPermission
179
+ const pathStr = params?.path ?? params?.file_path;
180
+ const isSkillRead = flags.skillReadCheck &&
181
+ toolName.toLowerCase() === "read" &&
182
+ isSkillReadPath(pathStr);
183
+ const skillName = isSkillRead
184
+ ? skillPathStore.getSkillNameForPath(sessionKey, String(pathStr ?? ""))
185
+ : undefined;
186
+ const needsCheckPermission = (flags.toolCheck && !isSkillRead) ||
187
+ (flags.skillReadCheck && isSkillRead && skillName != null);
188
+ // Low-risk tools skip CheckPermission + risk (not skill reads with a known name)
189
+ if (c.lowRiskBypass && isLowRiskTool(toolName, c.extraLowRisk) && !(isSkillRead && skillName)) {
190
+ logDebug(logger, `authz: low-risk bypass for ${toolName}`);
191
+ return undefined;
192
+ }
193
+ // TIP is guaranteed by session check; re-fetch is a cheap in-memory cache hit
194
+ const tip = await getOrRefreshTIPToken(c.storeDir, effectiveKey, c.tipRefreshOptions ? { ...c.tipRefreshOptions, ctxAgentId: c.agentId } : undefined);
195
+ // CheckPermission (tool or skill resource)
196
+ if (needsCheckPermission && c.identityClient && tip) {
197
+ const chain = extractDelegationChainFromJwt(tip.token);
198
+ if (!chain) {
199
+ return { block: true, blockReason: `${LOG_PREFIX} failed to parse delegation chain from TIP token` };
171
200
  }
172
- const fullHash = toolApprovalStore.hashToolParams(toolName, paramsRecord);
173
- const approvalId = toolApprovalStore.shortApprovalId(fullHash + effectiveKey + Date.now());
174
- const ttlSeconds = Math.floor(approvalTtlMs / 1000);
175
- if (supportsSyncApproval(sessionKey) && sendToSession) {
176
- toolApprovalStore.createPending({
177
- approvalId,
178
- sessionKey: effectiveKey,
179
- toolName,
180
- params: paramsRecord,
181
- ttlMs: approvalTtlMs,
201
+ const resource = skillName != null
202
+ ? { Type: "skill", Id: skillName }
203
+ : { Type: "tool", Id: toolName };
204
+ const originalCallers = chain.actors.slice().reverse().map((id) => ({
205
+ Type: "agent",
206
+ Id: id,
207
+ }));
208
+ logDebug(logger, `authz: CheckPermission for ${resource.Type}:${resource.Id} (sub: ${tip.sub})`);
209
+ try {
210
+ const result = await c.identityClient.checkPermission({
211
+ namespaceName: c.namespaceName,
212
+ principal: { Type: "user", Id: chain.principalId },
213
+ action: { Type: "Action", Id: "invoke" },
214
+ resource,
215
+ originalCallers,
182
216
  });
183
- await sendToSession(sessionKey, buildApprovalMessage(toolName, paramsRecord, approvalId, ttlSeconds, riskReason));
184
- const approved = await toolApprovalStore.pollForApproval(approvalId, approvalTtlMs);
185
- if (approved) {
186
- logDebug(logger, `AuthZ ok for ${toolName} (approved via poll)`);
187
- return;
217
+ if (!result.allowed) {
218
+ return {
219
+ block: true,
220
+ blockReason: result.message || `${LOG_PREFIX} CheckPermission denied for ${resource.Type} ${resource.Id}`,
221
+ };
188
222
  }
189
- return {
190
- block: true,
191
- blockReason: riskReason
192
- ? `AuthZ: High-risk tool requires approval. ${riskReason} Approval timed out or rejected.`
193
- : "AuthZ: High-risk tool requires approval. Approval timed out or rejected.",
194
- };
195
223
  }
196
- toolApprovalStore.createPending({
197
- approvalId,
198
- sessionKey: effectiveKey,
199
- toolName,
200
- params: paramsRecord,
201
- ttlMs: approvalTtlMs,
202
- });
203
- const reasonSuffix = riskReason ? ` Reason: ${riskReason}.` : "";
224
+ catch (err) {
225
+ return { block: true, blockReason: `${LOG_PREFIX} Failed to verify permission: ${String(err)}` };
226
+ }
227
+ if (skillName != null)
228
+ return undefined;
229
+ }
230
+ // Risk approval
231
+ if (!flags.requireRiskApproval)
232
+ return undefined;
233
+ const paramsRecord = params && typeof params === "object" ? params : {};
234
+ const llmConfig = flags.enableLlmRiskCheck && c.authz?.llmRiskCheck ? c.authz.llmRiskCheck : undefined;
235
+ const { risk, reason: riskReason } = await diagnoseRisk(toolName, paramsRecord, llmConfig, logger);
236
+ if (risk !== "high")
237
+ return undefined;
238
+ if (toolApprovalStore.hasRecentApproval(effectiveKey, toolName, paramsRecord)) {
239
+ toolApprovalStore.consumeApproval(effectiveKey, toolName, paramsRecord);
240
+ return undefined;
241
+ }
242
+ const fullHash = toolApprovalStore.hashToolParams(toolName, paramsRecord);
243
+ const approvalId = toolApprovalStore.shortApprovalId(fullHash + effectiveKey + Date.now());
244
+ const ttlSeconds = Math.floor(c.approvalTtlMs / 1000);
245
+ toolApprovalStore.createPending({
246
+ approvalId,
247
+ sessionKey: effectiveKey,
248
+ toolName,
249
+ params: paramsRecord,
250
+ ttlMs: c.approvalTtlMs,
251
+ });
252
+ if (supportsSyncApproval(sessionKey) && c.sendToSession) {
253
+ await c.sendToSession(sessionKey, buildApprovalMessage(toolName, paramsRecord, approvalId, ttlSeconds, riskReason));
254
+ const approved = await toolApprovalStore.pollForApproval(approvalId, c.approvalTtlMs);
255
+ if (approved)
256
+ return undefined;
204
257
  return {
205
258
  block: true,
206
- blockReason: `AuthZ: High-risk tool requires approval. Run /identity approve ${approvalId}, then retry.${reasonSuffix}`,
259
+ blockReason: riskReason
260
+ ? `${LOG_PREFIX} High-risk tool requires approval. ${riskReason} Approval timed out or rejected.`
261
+ : `${LOG_PREFIX} High-risk tool requires approval. Approval timed out or rejected.`,
207
262
  };
263
+ }
264
+ const reasonSuffix = riskReason ? ` Reason: ${riskReason}.` : "";
265
+ return {
266
+ block: true,
267
+ blockReason: `${LOG_PREFIX} High-risk tool requires approval. Run /identity approve ${approvalId}, then retry.${reasonSuffix}`,
208
268
  };
209
269
  }
270
+ /** Exposed for testing. */
271
+ export const __testing = { resolveCredentials };
@@ -34,7 +34,7 @@ export function createSubagentEndedCleanupHandler(deps) {
34
34
  if (!targetSessionKey)
35
35
  return;
36
36
  try {
37
- await deleteTIPToken(storeDir, targetSessionKey);
37
+ deleteTIPToken(targetSessionKey);
38
38
  await deleteSession(storeDir, targetSessionKey);
39
39
  logDebug(logger, `cleaned up TIP and session for ${targetSessionKey.slice(0, 24)}... on subagent_ended`);
40
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"low-risk-tools.d.ts","sourceRoot":"","sources":["../../../src/risk/low-risk-tools.ts"],"names":[],"mappings":"AAqCA,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAQhF"}
1
+ {"version":3,"file":"low-risk-tools.d.ts","sourceRoot":"","sources":["../../../src/risk/low-risk-tools.ts"],"names":[],"mappings":"AAmCA,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAQhF"}
@@ -20,8 +20,6 @@ const LOW_RISK_TOOL_NAMES = new Set([
20
20
  "memory_search",
21
21
  "memory_get",
22
22
  "session_status",
23
- "identity_whoami",
24
- "identity_status",
25
23
  "identity_list_tips",
26
24
  "identity_list_credentials",
27
25
  "identity_config",
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import type { IdentityService } from "./identity-service.js";
6
6
  export type FetchAndStoreTIPParams = {
7
- storeDir: string;
8
7
  sessionKey: string;
9
8
  identityService: IdentityService;
10
9
  jwtForExchange: string;
@@ -1 +1 @@
1
- {"version":3,"file":"tip-acquisition.d.ts","sourceRoot":"","sources":["../../../src/services/tip-acquisition.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAO7D,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,eAAe,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BpF"}
1
+ {"version":3,"file":"tip-acquisition.d.ts","sourceRoot":"","sources":["../../../src/services/tip-acquisition.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAO7D,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,eAAe,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BpF"}
@@ -16,7 +16,7 @@
16
16
  import { setTIPToken } from "../store/tip-store.js";
17
17
  import { resolveAgentId, resolveWorkloadNameForSession, } from "../utils/derive-session-key.js";
18
18
  export async function fetchAndStoreTIP(params) {
19
- const { storeDir, sessionKey, identityService, jwtForExchange, sub, ctxAgentId, configWorkloadName, targetWorkloadSessionKey, parentSessionKey, } = params;
19
+ const { sessionKey, identityService, jwtForExchange, sub, ctxAgentId, configWorkloadName, targetWorkloadSessionKey, parentSessionKey, } = params;
20
20
  const workloadKey = targetWorkloadSessionKey ?? sessionKey;
21
21
  const agentId = resolveAgentId({ agentId: ctxAgentId, sessionKey: workloadKey });
22
22
  const workloadName = resolveWorkloadNameForSession({
@@ -35,5 +35,5 @@ export async function fetchAndStoreTIP(params) {
35
35
  ...(parentSessionKey && { parentSessionKey }),
36
36
  issuedAt: Date.now(),
37
37
  };
38
- await setTIPToken(storeDir, sessionKey, entry);
38
+ setTIPToken(sessionKey, entry);
39
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tip-propagation.d.ts","sourceRoot":"","sources":["../../../src/services/tip-propagation.ts"],"names":[],"mappings":"AAgBA;;GAEG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAM3D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,eAAe,CAAC;IACjC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAClD,MAAM,EAAE;QAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CACzE,CAAC;AAEF;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2CpF"}
1
+ {"version":3,"file":"tip-propagation.d.ts","sourceRoot":"","sources":["../../../src/services/tip-propagation.ts"],"names":[],"mappings":"AAgBA;;GAEG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAM3D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,eAAe,CAAC;IACjC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAClD,MAAM,EAAE;QAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CACzE,CAAC;AAEF;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CpF"}
@@ -30,7 +30,6 @@ export async function propagateTIPToTarget(params) {
30
30
  }
31
31
  if (subagentTipPropagation === true) {
32
32
  await fetchAndStoreTIP({
33
- storeDir,
34
33
  sessionKey: targetSessionKey,
35
34
  identityService,
36
35
  jwtForExchange: callerTIP.token,
@@ -42,7 +41,7 @@ export async function propagateTIPToTarget(params) {
42
41
  logInfo(logger, `TIP propagated to ${targetSessionKey.slice(0, 24)}...`);
43
42
  }
44
43
  else {
45
- await setTIPToken(storeDir, targetSessionKey, {
44
+ setTIPToken(targetSessionKey, {
46
45
  ...callerTIP,
47
46
  ...(callerSessionKey && { parentSessionKey: callerSessionKey }),
48
47
  });
@@ -20,5 +20,5 @@ export type GetOrRefreshTIPOptions = {
20
20
  * Get TIP token for session. If missing or expired and refresh options provided,
21
21
  * attempts to fetch TIP (refreshing userToken if needed).
22
22
  */
23
- export declare function getOrRefreshTIPToken(storeDir: string, sessionKey: string, options?: GetOrRefreshTIPOptions): Promise<Awaited<ReturnType<typeof getTIPToken>>>;
23
+ export declare function getOrRefreshTIPToken(storeDir: string, sessionKey: string, options?: GetOrRefreshTIPOptions): Promise<ReturnType<typeof getTIPToken>>;
24
24
  //# sourceMappingURL=tip-with-refresh.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tip-with-refresh.d.ts","sourceRoot":"","sources":["../../../src/services/tip-with-refresh.ts"],"names":[],"mappings":"AAgBA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAMpD,MAAM,MAAM,sBAAsB,GAAG;IACnC,eAAe,EAAE,eAAe,CAAC;IACjC,uBAAuB,CAAC,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC9D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAC1E,CAAC;AAEF;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAgElD"}
1
+ {"version":3,"file":"tip-with-refresh.d.ts","sourceRoot":"","sources":["../../../src/services/tip-with-refresh.ts"],"names":[],"mappings":"AAgBA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAMpD,MAAM,MAAM,sBAAsB,GAAG;IACnC,eAAe,EAAE,eAAe,CAAC;IACjC,uBAAuB,CAAC,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC9D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAC1E,CAAC;AAEF;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CA8DzC"}
@@ -24,7 +24,7 @@ import { fetchAndStoreTIP } from "./tip-acquisition.js";
24
24
  * attempts to fetch TIP (refreshing userToken if needed).
25
25
  */
26
26
  export async function getOrRefreshTIPToken(storeDir, sessionKey, options) {
27
- const cached = await getTIPToken(storeDir, sessionKey);
27
+ const cached = getTIPToken(sessionKey);
28
28
  if (cached)
29
29
  return cached;
30
30
  if (!options) {
@@ -36,7 +36,6 @@ export async function getOrRefreshTIPToken(storeDir, sessionKey, options) {
36
36
  return null;
37
37
  try {
38
38
  await fetchAndStoreTIP({
39
- storeDir,
40
39
  sessionKey,
41
40
  identityService,
42
41
  jwtForExchange: session.userToken,
@@ -45,7 +44,7 @@ export async function getOrRefreshTIPToken(storeDir, sessionKey, options) {
45
44
  configWorkloadName,
46
45
  });
47
46
  logInfo(logger, `TIP acquired for ${sessionKey.slice(0, 24)}...`);
48
- return getTIPToken(storeDir, sessionKey);
47
+ return getTIPToken(sessionKey);
49
48
  }
50
49
  catch (err) {
51
50
  const canRefresh = isTokenExpiredError(err) && !!getOidcConfigForRefresh && !!session.refreshToken;
@@ -60,7 +59,6 @@ export async function getOrRefreshTIPToken(storeDir, sessionKey, options) {
60
59
  return null;
61
60
  try {
62
61
  await fetchAndStoreTIP({
63
- storeDir,
64
62
  sessionKey,
65
63
  identityService,
66
64
  jwtForExchange: refreshed,
@@ -69,7 +67,7 @@ export async function getOrRefreshTIPToken(storeDir, sessionKey, options) {
69
67
  configWorkloadName,
70
68
  });
71
69
  logInfo(logger, `TIP acquired after refresh for ${sessionKey.slice(0, 24)}...`);
72
- return getTIPToken(storeDir, sessionKey);
70
+ return getTIPToken(sessionKey);
73
71
  }
74
72
  catch {
75
73
  return null;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * In-memory env snapshot store for per-tool-call credential injection.
3
+ *
4
+ * before_tool_call applies a snapshot (saves baseline, sets env);
5
+ * after_tool_call restores the snapshot (reverts env to baseline).
6
+ * Keyed by "runId:toolCallId" so concurrent tool calls stay isolated.
7
+ *
8
+ * Each env key has its own promise-chain mutex so concurrent tool calls
9
+ * writing the SAME key are serialized, while calls using different keys
10
+ * run fully in parallel.
11
+ */
12
+ type EnvSnapshot = {
13
+ /** envVar → previous value (undefined = key was absent) */
14
+ baseline: Map<string, string | undefined>;
15
+ /** Release functions for per-key locks (call on restore) */
16
+ releasers: Array<() => void>;
17
+ createdAt: number;
18
+ };
19
+ export declare function snapshotKey(runId?: string, toolCallId?: string): string;
20
+ export declare function hasSnapshot(runId?: string, toolCallId?: string): boolean;
21
+ /**
22
+ * Acquire per-key locks, record baseline, then overwrite process.env.
23
+ * Keys are sorted before acquisition to prevent deadlocks when two
24
+ * concurrent calls compete for the same set of keys.
25
+ */
26
+ export declare function applyEnvSnapshot(credentials: Record<string, string>, runId?: string, toolCallId?: string): Promise<void>;
27
+ /**
28
+ * Revert process.env to baseline values, then release per-key locks
29
+ * so the next queued tool call for each key can proceed.
30
+ */
31
+ export declare function restoreEnvSnapshot(runId?: string, toolCallId?: string): void;
32
+ /** Exposed for testing. */
33
+ export declare const __testing: {
34
+ snapshots: Map<string, EnvSnapshot>;
35
+ envKeyChains: Map<string, Promise<void>>;
36
+ };
37
+ export {};
38
+ //# sourceMappingURL=credential-env-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-env-snapshot.d.ts","sourceRoot":"","sources":["../../../src/store/credential-env-snapshot.ts"],"names":[],"mappings":"AAgBA;;;;;;;;;;GAUG;AAEH,KAAK,WAAW,GAAG;IACjB,2DAA2D;IAC3D,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC1C,4DAA4D;IAC5D,SAAS,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAyBF,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAExE;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,KAAK,CAAC,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAc5E;AAoBD,2BAA2B;AAC3B,eAAO,MAAM,SAAS;;;CAA8B,CAAC"}