@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.
- package/README-cn.md +19 -9
- package/README.md +22 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -17
- package/dist/src/actions/identity-actions.d.ts +7 -1
- package/dist/src/actions/identity-actions.d.ts.map +1 -1
- package/dist/src/actions/identity-actions.js +82 -34
- package/dist/src/commands/identity-commands.d.ts.map +1 -1
- package/dist/src/commands/identity-commands.js +9 -4
- package/dist/src/hooks/after-tool-call.d.ts +12 -0
- package/dist/src/hooks/after-tool-call.d.ts.map +1 -0
- package/dist/src/hooks/after-tool-call.js +31 -0
- package/dist/src/hooks/before-agent-start.d.ts +3 -3
- package/dist/src/hooks/before-agent-start.d.ts.map +1 -1
- package/dist/src/hooks/before-agent-start.js +6 -20
- package/dist/src/hooks/before-tool-call.d.ts +19 -14
- package/dist/src/hooks/before-tool-call.d.ts.map +1 -1
- package/dist/src/hooks/before-tool-call.js +192 -130
- package/dist/src/hooks/subagent-ended-cleanup.js +1 -1
- package/dist/src/risk/low-risk-tools.d.ts.map +1 -1
- package/dist/src/risk/low-risk-tools.js +0 -2
- package/dist/src/services/tip-acquisition.d.ts +0 -1
- package/dist/src/services/tip-acquisition.d.ts.map +1 -1
- package/dist/src/services/tip-acquisition.js +2 -2
- package/dist/src/services/tip-propagation.d.ts.map +1 -1
- package/dist/src/services/tip-propagation.js +1 -2
- package/dist/src/services/tip-with-refresh.d.ts +1 -1
- package/dist/src/services/tip-with-refresh.d.ts.map +1 -1
- package/dist/src/services/tip-with-refresh.js +3 -5
- package/dist/src/store/credential-env-snapshot.d.ts +38 -0
- package/dist/src/store/credential-env-snapshot.d.ts.map +1 -0
- package/dist/src/store/credential-env-snapshot.js +101 -0
- package/dist/src/store/encryption.d.ts +30 -0
- package/dist/src/store/encryption.d.ts.map +1 -0
- package/dist/src/store/encryption.js +147 -0
- package/dist/src/store/session-store.d.ts.map +1 -1
- package/dist/src/store/session-store.js +91 -8
- package/dist/src/store/tip-store.d.ts +11 -6
- package/dist/src/store/tip-store.d.ts.map +1 -1
- package/dist/src/store/tip-store.js +23 -54
- package/dist/src/tools/identity-fetch.d.ts.map +1 -1
- package/dist/src/tools/identity-fetch.js +1 -0
- package/dist/src/tools/identity-list-credentials.d.ts +2 -0
- package/dist/src/tools/identity-list-credentials.d.ts.map +1 -1
- package/dist/src/tools/identity-list-credentials.js +6 -3
- package/package.json +1 -1
- 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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
29
|
-
skillReadCheck
|
|
30
|
-
requireRiskApproval
|
|
31
|
-
enableLlmRiskCheck
|
|
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
|
-
|
|
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
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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:
|
|
125
|
+
blockReason: `${LOG_PREFIX} session has no valid identity (TIP token required)`,
|
|
116
126
|
};
|
|
117
127
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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 (
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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:
|
|
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
|
-
|
|
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":"
|
|
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"}
|
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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<
|
|
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,
|
|
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 =
|
|
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(
|
|
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(
|
|
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"}
|