@m1a0rz/agent-identity 0.4.0 → 0.4.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 (127) hide show
  1. package/README-cn.md +34 -0
  2. package/README.md +34 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +91 -6
  5. package/dist/scripts/demo-get-session.d.ts +15 -0
  6. package/dist/scripts/demo-get-session.d.ts.map +1 -0
  7. package/dist/scripts/demo-get-session.js +58 -0
  8. package/dist/src/actions/identity-actions.d.ts +74 -8
  9. package/dist/src/actions/identity-actions.d.ts.map +1 -1
  10. package/dist/src/actions/identity-actions.js +209 -83
  11. package/dist/src/commands/identity-commands.d.ts.map +1 -1
  12. package/dist/src/commands/identity-commands.js +139 -11
  13. package/dist/src/gateway/identity-session-methods.d.ts +67 -0
  14. package/dist/src/gateway/identity-session-methods.d.ts.map +1 -0
  15. package/dist/src/gateway/identity-session-methods.js +134 -0
  16. package/dist/src/hooks/after-tool-call.d.ts.map +1 -1
  17. package/dist/src/hooks/after-tool-call.js +12 -0
  18. package/dist/src/hooks/before-agent-start.d.ts +2 -0
  19. package/dist/src/hooks/before-agent-start.d.ts.map +1 -1
  20. package/dist/src/hooks/before-agent-start.js +33 -6
  21. package/dist/src/hooks/before-tool-call.d.ts +1 -0
  22. package/dist/src/hooks/before-tool-call.d.ts.map +1 -1
  23. package/dist/src/hooks/before-tool-call.js +29 -5
  24. package/dist/src/hooks/llm-input.d.ts.map +1 -1
  25. package/dist/src/hooks/llm-input.js +32 -4
  26. package/dist/src/hooks/sessions-send-propagation.d.ts.map +1 -1
  27. package/dist/src/hooks/sessions-send-propagation.js +1 -0
  28. package/dist/src/hooks/sessions-spawn-propagation.d.ts.map +1 -1
  29. package/dist/src/hooks/sessions-spawn-propagation.js +1 -0
  30. package/dist/src/hooks/tool-result-persist.d.ts +20 -0
  31. package/dist/src/hooks/tool-result-persist.d.ts.map +1 -0
  32. package/dist/src/hooks/tool-result-persist.js +50 -0
  33. package/dist/src/preflight/plugin-preflight.d.ts +55 -0
  34. package/dist/src/preflight/plugin-preflight.d.ts.map +1 -0
  35. package/dist/src/preflight/plugin-preflight.js +226 -0
  36. package/dist/src/preflight/plugin-state.d.ts +18 -0
  37. package/dist/src/preflight/plugin-state.d.ts.map +1 -0
  38. package/dist/src/preflight/plugin-state.js +19 -0
  39. package/dist/src/routes/oidc-login.js +2 -2
  40. package/dist/src/services/identity-client.d.ts +106 -1
  41. package/dist/src/services/identity-client.d.ts.map +1 -1
  42. package/dist/src/services/identity-client.js +123 -1
  43. package/dist/src/services/identity-credentials.d.ts +1 -1
  44. package/dist/src/services/identity-credentials.d.ts.map +1 -1
  45. package/dist/src/services/identity-credentials.js +32 -16
  46. package/dist/src/services/oidc-client.d.ts +12 -1
  47. package/dist/src/services/oidc-client.d.ts.map +1 -1
  48. package/dist/src/services/oidc-client.js +20 -3
  49. package/dist/src/services/session-refresh.d.ts +10 -0
  50. package/dist/src/services/session-refresh.d.ts.map +1 -1
  51. package/dist/src/services/session-refresh.js +29 -5
  52. package/dist/src/services/skill-contract-metadata.d.ts +35 -0
  53. package/dist/src/services/skill-contract-metadata.d.ts.map +1 -0
  54. package/dist/src/services/skill-contract-metadata.js +145 -0
  55. package/dist/src/services/skill-contract-renderer.d.ts +14 -0
  56. package/dist/src/services/skill-contract-renderer.d.ts.map +1 -0
  57. package/dist/src/services/skill-contract-renderer.js +120 -0
  58. package/dist/src/services/tip-propagation.d.ts +2 -0
  59. package/dist/src/services/tip-propagation.d.ts.map +1 -1
  60. package/dist/src/services/tip-propagation.js +4 -3
  61. package/dist/src/services/tip-with-refresh.d.ts +1 -1
  62. package/dist/src/services/tip-with-refresh.d.ts.map +1 -1
  63. package/dist/src/services/tip-with-refresh.js +24 -39
  64. package/dist/src/store/credential-store.d.ts +6 -1
  65. package/dist/src/store/credential-store.d.ts.map +1 -1
  66. package/dist/src/store/credential-store.js +3 -0
  67. package/dist/src/store/oidc-state-store.d.ts +3 -3
  68. package/dist/src/store/oidc-state-store.d.ts.map +1 -1
  69. package/dist/src/store/oidc-state-store.js +2 -2
  70. package/dist/src/store/sender-session-store.d.ts +8 -0
  71. package/dist/src/store/sender-session-store.d.ts.map +1 -1
  72. package/dist/src/store/sender-session-store.js +46 -11
  73. package/dist/src/store/skill-contract-store.d.ts +19 -0
  74. package/dist/src/store/skill-contract-store.d.ts.map +1 -0
  75. package/dist/src/store/skill-contract-store.js +65 -0
  76. package/dist/src/store/skill-path-store.d.ts +5 -0
  77. package/dist/src/store/skill-path-store.d.ts.map +1 -1
  78. package/dist/src/store/skill-path-store.js +13 -1
  79. package/dist/src/tools/identity-approve-tool.d.ts +2 -11
  80. package/dist/src/tools/identity-approve-tool.d.ts.map +1 -1
  81. package/dist/src/tools/identity-config-suggest.d.ts +2 -13
  82. package/dist/src/tools/identity-config-suggest.d.ts.map +1 -1
  83. package/dist/src/tools/identity-config.d.ts +2 -7
  84. package/dist/src/tools/identity-config.d.ts.map +1 -1
  85. package/dist/src/tools/identity-fetch.d.ts +2 -13
  86. package/dist/src/tools/identity-fetch.d.ts.map +1 -1
  87. package/dist/src/tools/identity-fetch.js +3 -3
  88. package/dist/src/tools/identity-get-role-credentials.d.ts +10 -0
  89. package/dist/src/tools/identity-get-role-credentials.d.ts.map +1 -0
  90. package/dist/src/tools/identity-get-role-credentials.js +56 -0
  91. package/dist/src/tools/identity-get-session-token.d.ts +8 -0
  92. package/dist/src/tools/identity-get-session-token.d.ts.map +1 -0
  93. package/dist/src/tools/identity-get-session-token.js +46 -0
  94. package/dist/src/tools/identity-get-tip-token.d.ts +8 -0
  95. package/dist/src/tools/identity-get-tip-token.d.ts.map +1 -0
  96. package/dist/src/tools/identity-get-tip-token.js +46 -0
  97. package/dist/src/tools/identity-list-credentials.d.ts +2 -11
  98. package/dist/src/tools/identity-list-credentials.d.ts.map +1 -1
  99. package/dist/src/tools/identity-list-credentials.js +4 -3
  100. package/dist/src/tools/identity-list-risk-patterns.d.ts +2 -7
  101. package/dist/src/tools/identity-list-risk-patterns.d.ts.map +1 -1
  102. package/dist/src/tools/identity-list-roles.d.ts +8 -0
  103. package/dist/src/tools/identity-list-roles.d.ts.map +1 -0
  104. package/dist/src/tools/identity-list-roles.js +43 -0
  105. package/dist/src/tools/identity-list-tips.d.ts +2 -7
  106. package/dist/src/tools/identity-list-tips.d.ts.map +1 -1
  107. package/dist/src/tools/identity-login.d.ts +2 -7
  108. package/dist/src/tools/identity-login.d.ts.map +1 -1
  109. package/dist/src/tools/identity-logout.d.ts +2 -7
  110. package/dist/src/tools/identity-logout.d.ts.map +1 -1
  111. package/dist/src/tools/identity-risk-check.d.ts +3 -17
  112. package/dist/src/tools/identity-risk-check.d.ts.map +1 -1
  113. package/dist/src/tools/identity-set-binding.d.ts +2 -10
  114. package/dist/src/tools/identity-set-binding.d.ts.map +1 -1
  115. package/dist/src/tools/identity-status.d.ts +2 -7
  116. package/dist/src/tools/identity-status.d.ts.map +1 -1
  117. package/dist/src/tools/identity-unset-binding.d.ts +2 -9
  118. package/dist/src/tools/identity-unset-binding.d.ts.map +1 -1
  119. package/dist/src/tools/identity-whoami.d.ts +2 -7
  120. package/dist/src/tools/identity-whoami.d.ts.map +1 -1
  121. package/dist/src/types.d.ts +21 -0
  122. package/dist/src/types.d.ts.map +1 -1
  123. package/dist/src/utils/derive-session-key.d.ts +11 -1
  124. package/dist/src/utils/derive-session-key.d.ts.map +1 -1
  125. package/dist/src/utils/derive-session-key.js +46 -6
  126. package/openclaw.plugin.json +18 -0
  127. package/package.json +33 -7
@@ -18,7 +18,8 @@ import { getOrRefreshTIPToken } from "../services/tip-with-refresh.js";
18
18
  import { fetchOIDCDiscovery, buildAuthorizationUrl, generateState, generatePKCE, generateNonce, } from "../services/oidc-client.js";
19
19
  import { loadCredentialEnvBindings, loadAllCredentialEnvBindings, setCredentialEnvBinding, deleteCredentialEnvBinding, } from "../store/credential-env-bindings.js";
20
20
  import { loadCredentials, setCredential, getCredential, deleteCredentialsForSession, } from "../store/credential-store.js";
21
- import { getSession, deleteSession } from "../store/session-store.js";
21
+ import { deleteSession } from "../store/session-store.js";
22
+ import { getSessionWithRefresh } from "../services/session-refresh.js";
22
23
  import { createState } from "../store/oidc-state-store.js";
23
24
  import { getAllTIPTokens, deleteTIPToken } from "../store/tip-store.js";
24
25
  import { extractDelegationChainFromJwt } from "../utils/auth.js";
@@ -30,6 +31,34 @@ function inferFlowFromProvider(info) {
30
31
  return "oauth2-m2m";
31
32
  return "oauth2-user";
32
33
  }
34
+ function buildTipRefreshOptions(deps, ctxAgentId, errorHolder) {
35
+ if (!deps.getOidcConfigForRefresh)
36
+ return undefined;
37
+ return {
38
+ identityService: deps.identityService,
39
+ getOidcConfigForRefresh: deps.getOidcConfigForRefresh,
40
+ configWorkloadName: deps.configWorkloadName,
41
+ ctxAgentId,
42
+ logger: deps.logger,
43
+ ...(errorHolder ? { errorHolder } : {}),
44
+ };
45
+ }
46
+ async function resolveAndRefreshTIP(deps, sessionKey, config, errorHolder) {
47
+ const ctxAgentId = resolveAgentId({ sessionKey, config: config });
48
+ return getOrRefreshTIPToken(deps.storeDir, sessionKey, buildTipRefreshOptions(deps, ctxAgentId, errorHolder));
49
+ }
50
+ function formatCredentialStatus(c) {
51
+ if (!c)
52
+ return "—";
53
+ if (c.type === "oauth2")
54
+ return c.status;
55
+ if (c.type === "api_key") {
56
+ if (c.valueFromEnv)
57
+ return `from \`${c.valueFromEnv}\``;
58
+ return c.value ? "✓" : "empty";
59
+ }
60
+ return "—";
61
+ }
33
62
  const OAUTH_POLL_INTERVAL_MS = 2500;
34
63
  const OAUTH_POLL_TIMEOUT_MS = 10 * 60 * 1000;
35
64
  const OAUTH_QUICK_RETRY_MS = 1000;
@@ -70,19 +99,9 @@ async function pollOAuthAndNotify(params) {
70
99
  await sendCredentialMessage?.(target, `⚠️ Authorization timed out for \`${provider}\`. Run \`/identity fetch ${provider}\` again.`);
71
100
  }
72
101
  export async function runStatus(deps, sessionKey, config) {
73
- const { storeDir, identityService, getOidcConfigForRefresh, configWorkloadName, logger } = deps;
74
- const session = await getSession(storeDir, sessionKey);
75
- const ctxAgentId = resolveAgentId({ sessionKey, config: config });
76
- const tipRefreshOptions = getOidcConfigForRefresh
77
- ? {
78
- identityService,
79
- getOidcConfigForRefresh,
80
- configWorkloadName,
81
- ctxAgentId,
82
- logger,
83
- }
84
- : undefined;
85
- const tip = await getOrRefreshTIPToken(storeDir, sessionKey, tipRefreshOptions);
102
+ const { storeDir } = deps;
103
+ const session = await getSessionWithRefresh(storeDir, sessionKey, deps.getOidcConfigForRefresh);
104
+ const tip = await resolveAndRefreshTIP(deps, sessionKey, config);
86
105
  const credentials = await loadCredentials(storeDir, sessionKey);
87
106
  const bindings = await loadCredentialEnvBindings(storeDir, sessionKey);
88
107
  const tipChain = tip
@@ -92,7 +111,7 @@ export async function runStatus(deps, sessionKey, config) {
92
111
  })()
93
112
  : undefined;
94
113
  return {
95
- loggedIn: !!(session && identityService.parseUserToken(session.userToken).valid),
114
+ loggedIn: !!(session?.userToken),
96
115
  sub: session?.sub ?? null,
97
116
  hasTip: Boolean(tip),
98
117
  sessionLoginAt: session?.loginAt,
@@ -108,22 +127,10 @@ export async function runLogin(deps, sessionKey, options) {
108
127
  const { storeDir, identityService, getOidcConfig, logger } = deps;
109
128
  const config = options?.config;
110
129
  const deliveryTarget = options?.deliveryTarget ?? null;
111
- const session = await getSession(storeDir, sessionKey);
112
- const hasValidCred = session && identityService.parseUserToken(session.userToken).valid;
113
- if (hasValidCred && session) {
114
- const ctxAgentId = resolveAgentId({ sessionKey, config: config });
130
+ const session = await getSessionWithRefresh(storeDir, sessionKey, deps.getOidcConfigForRefresh);
131
+ if (session) {
115
132
  const errorHolder = {};
116
- const tipRefreshOptions = deps.getOidcConfigForRefresh
117
- ? {
118
- identityService,
119
- getOidcConfigForRefresh: deps.getOidcConfigForRefresh,
120
- configWorkloadName: deps.configWorkloadName,
121
- ctxAgentId,
122
- logger,
123
- errorHolder,
124
- }
125
- : undefined;
126
- const tip = await getOrRefreshTIPToken(storeDir, sessionKey, tipRefreshOptions);
133
+ const tip = await resolveAndRefreshTIP(deps, sessionKey, config, errorHolder);
127
134
  if (tip) {
128
135
  return { kind: "already_logged_in", sub: session.sub };
129
136
  }
@@ -138,10 +145,13 @@ export async function runLogin(deps, sessionKey, options) {
138
145
  try {
139
146
  const oidcConfig = await getOidcConfig();
140
147
  const discovery = await fetchOIDCDiscovery(oidcConfig.discoveryUrl);
141
- const state = await generateState();
148
+ // Prefer explicit config, then fall back to what resolveOIDCConfig already cached
149
+ const identityProvider = deps.pluginConfig?.userpool?.identityProvider ?? oidcConfig.identityProvider;
150
+ const workloadName = deps.configWorkloadName;
151
+ const state = await generateState({ target: workloadName });
142
152
  const { codeVerifier, codeChallenge } = await generatePKCE();
143
153
  const nonce = await generateNonce();
144
- await createState(storeDir, sessionKey, "", state, deliveryTarget, { codeVerifier, nonce });
154
+ createState(sessionKey, "", state, deliveryTarget, { codeVerifier, nonce });
145
155
  const authUrl = buildAuthorizationUrl({
146
156
  authorizationEndpoint: discovery.authorization_endpoint,
147
157
  clientId: oidcConfig.clientId,
@@ -151,6 +161,8 @@ export async function runLogin(deps, sessionKey, options) {
151
161
  codeChallenge,
152
162
  codeChallengeMethod: "S256",
153
163
  nonce,
164
+ redirectRelayUri: oidcConfig.callbackUrl,
165
+ identityProvider,
154
166
  });
155
167
  logInfo(logger, `login returning IdP URL for sessionKey=${sessionKey.slice(0, 24)}...`);
156
168
  return { kind: "auth_url", authUrl };
@@ -176,11 +188,11 @@ export async function runListCredentials(deps, sessionKey, page = 1, filter) {
176
188
  const { storeDir, identityClient, identityService, logger } = deps;
177
189
  const creds = await loadCredentials(storeDir, sessionKey);
178
190
  const bindings = await loadCredentialEnvBindings(storeDir, sessionKey);
179
- let providers = [];
191
+ let credProviders = [];
180
192
  let totalCount = 0;
181
193
  if (identityClient) {
182
- const session = await getSession(storeDir, sessionKey);
183
- if (!session || !identityService.parseUserToken(session.userToken).valid) {
194
+ const session = await getSessionWithRefresh(storeDir, sessionKey, deps.getOidcConfigForRefresh);
195
+ if (!session) {
184
196
  logWarn(logger, `list-credentials: no valid session for key=${sessionKey.slice(0, 24)}...`);
185
197
  }
186
198
  else {
@@ -190,12 +202,15 @@ export async function runListCredentials(deps, sessionKey, page = 1, filter) {
190
202
  apiFilter.Name = filter.name;
191
203
  if (filter?.flow)
192
204
  apiFilter.Flow = filter.flow;
205
+ if (filter?.type)
206
+ apiFilter.Type = filter.type;
193
207
  const result = await identityClient.listCredentialProviders({
194
208
  PageNumber: page,
195
209
  PageSize: LIST_PAGE_SIZE,
210
+ PoolName: deps.workloadPoolName,
196
211
  ...(Object.keys(apiFilter).length > 0 ? { Filter: apiFilter } : {}),
197
212
  });
198
- providers = result.CredentialProviders ?? result.Data ?? [];
213
+ credProviders = result.CredentialProviders ?? result.Data ?? [];
199
214
  totalCount = result.TotalCount ?? 0;
200
215
  }
201
216
  catch (e) {
@@ -203,46 +218,23 @@ export async function runListCredentials(deps, sessionKey, page = 1, filter) {
203
218
  }
204
219
  }
205
220
  }
206
- const providerNames = new Set(providers.map((p) => p.Name));
221
+ const providerNames = new Set(credProviders.map((p) => p.Name));
207
222
  const storedNames = Object.keys(creds);
208
- const providerRows = providers.map((p) => {
209
- const c = creds[p.Name];
210
- const bind = bindings[p.Name];
211
- const typeLabel = p.Type === "api_key" ? "api_key" : `oauth2${p.Flow === "M2M" ? " (m2m)" : ""}`;
212
- const status = c?.type === "oauth2"
213
- ? c.status
214
- : c?.type === "api_key"
215
- ? c.valueFromEnv
216
- ? `from \`${c.valueFromEnv}\``
217
- : c.value
218
- ? "✓"
219
- : "empty"
220
- : "—";
221
- return {
222
- name: p.Name,
223
- type: typeLabel,
224
- flow: p.Flow,
225
- status,
226
- binding: bind,
227
- };
228
- });
223
+ const providerRows = credProviders.map((p) => ({
224
+ name: p.Name,
225
+ type: p.Type === "api_key" ? "api_key" : `oauth2${p.Flow === "M2M" ? " (m2m)" : ""}`,
226
+ flow: p.Flow,
227
+ status: formatCredentialStatus(creds[p.Name]),
228
+ binding: bindings[p.Name],
229
+ }));
229
230
  const storedOnly = storedNames
230
231
  .filter((n) => !providerNames.has(n))
231
232
  .sort()
232
- .map((name) => {
233
- const c = creds[name];
234
- const bind = bindings[name];
235
- const status = c.type === "oauth2"
236
- ? c.status
237
- : c.type === "api_key"
238
- ? c.valueFromEnv
239
- ? `from \`${c.valueFromEnv}\``
240
- : c.value
241
- ? "✓"
242
- : "empty"
243
- : "—";
244
- return { name, status, binding: bind };
245
- });
233
+ .map((name) => ({
234
+ name,
235
+ status: formatCredentialStatus(creds[name]),
236
+ binding: bindings[name],
237
+ }));
246
238
  return {
247
239
  providers: providerRows,
248
240
  storedOnly,
@@ -251,6 +243,48 @@ export async function runListCredentials(deps, sessionKey, page = 1, filter) {
251
243
  totalCount,
252
244
  };
253
245
  }
246
+ export async function runListRoleCredentials(deps, sessionKey, filter) {
247
+ const { storeDir, identityClient, identityService, logger } = deps;
248
+ let roleProviders = [];
249
+ if (identityClient) {
250
+ const session = await getSessionWithRefresh(storeDir, sessionKey, deps.getOidcConfigForRefresh);
251
+ if (!session) {
252
+ logWarn(logger, `list-roles: no valid session for key=${sessionKey.slice(0, 24)}...`);
253
+ }
254
+ else {
255
+ try {
256
+ const roleResult = await identityClient.listRoleCredentialProviders({
257
+ PageNumber: 1,
258
+ PageSize: 50,
259
+ PoolName: deps.workloadPoolName,
260
+ UserPoolName: deps.userPoolName,
261
+ });
262
+ roleProviders = roleResult.RoleCredentialProviders ?? [];
263
+ if (filter?.name) {
264
+ const n = filter.name.toLowerCase();
265
+ roleProviders = roleProviders.filter((rp) => rp.Name.toLowerCase().includes(n));
266
+ }
267
+ }
268
+ catch (e) {
269
+ logWarn(logger, `list-role-credential-providers API error: ${String(e)}`);
270
+ }
271
+ }
272
+ }
273
+ const providerRows = roleProviders.map((rp) => {
274
+ const pool = rp.Config?.IdentityPool;
275
+ const identitySource = pool?.UserPool?.PoolName
276
+ ? `userpool:${pool.UserPool.PoolName}`
277
+ : pool?.AgentPool?.PoolName
278
+ ? `agentpool:${pool.AgentPool.PoolName}`
279
+ : undefined;
280
+ return { name: rp.Name, identitySource };
281
+ });
282
+ return {
283
+ providers: providerRows,
284
+ page: 1,
285
+ hasMore: roleProviders.length >= 50,
286
+ };
287
+ }
254
288
  export async function runListTips(deps) {
255
289
  const { storeDir } = deps;
256
290
  const tokens = getAllTIPTokens();
@@ -318,17 +352,7 @@ export async function runFetch(deps, sessionKey, params) {
318
352
  if (!identityClient) {
319
353
  return { kind: "error", message: "Identity service not configured. Cannot add credentials." };
320
354
  }
321
- const ctxAgentId = resolveAgentId({ sessionKey, config: config });
322
- const tipRefreshOptions = deps.getOidcConfigForRefresh
323
- ? {
324
- identityService: deps.identityService,
325
- getOidcConfigForRefresh: deps.getOidcConfigForRefresh,
326
- configWorkloadName: deps.configWorkloadName,
327
- ctxAgentId,
328
- logger: deps.logger,
329
- }
330
- : undefined;
331
- const tip = await getOrRefreshTIPToken(storeDir, sessionKey, tipRefreshOptions);
355
+ const tip = await resolveAndRefreshTIP(deps, sessionKey, config);
332
356
  if (!tip) {
333
357
  return {
334
358
  kind: "error",
@@ -341,6 +365,7 @@ export async function runFetch(deps, sessionKey, params) {
341
365
  const listResult = await identityClient.listCredentialProviders({
342
366
  PageNumber: 1,
343
367
  PageSize: 20,
368
+ PoolName: deps.workloadPoolName,
344
369
  Filter: { Name: provider },
345
370
  });
346
371
  const list = listResult.CredentialProviders ?? listResult.Data ?? [];
@@ -365,6 +390,18 @@ export async function runFetch(deps, sessionKey, params) {
365
390
  });
366
391
  return { kind: "success", message: `✓ API key for \`${provider}\` added.` };
367
392
  }
393
+ if (effectiveFlow === "user") {
394
+ const result = await identityClient.getUserCredential({
395
+ credentialId: provider,
396
+ identityToken: tip.token,
397
+ });
398
+ await setCredential(storeDir, sessionKey, provider, {
399
+ type: "user",
400
+ key: result.credentialId,
401
+ value: result.credentialData,
402
+ });
403
+ return { kind: "success", message: `✓ User credential for \`${provider}\` added.` };
404
+ }
368
405
  const oauth2Flow = effectiveFlow === "oauth2-m2m" ? "M2M" : "USER_FEDERATION";
369
406
  const oauthResult = await identityClient.getResourceOauth2Token({
370
407
  providerName: provider,
@@ -498,3 +535,92 @@ export async function runUnsetBinding(deps, sessionKey, params) {
498
535
  await deleteCredentialEnvBinding(storeDir, sessionKey, provider);
499
536
  return { ok: true, message: `✓ Unset env binding for \`${provider}\`.` };
500
537
  }
538
+ export async function runGetRoleCredentials(deps, sessionKey, params) {
539
+ const { identityClient, storeDir, logger } = deps;
540
+ if (!identityClient) {
541
+ return { kind: "error", message: "Identity service not configured." };
542
+ }
543
+ let identityToken;
544
+ if (params.useTip) {
545
+ const tip = await resolveAndRefreshTIP(deps, sessionKey, params.config);
546
+ if (!tip) {
547
+ return {
548
+ kind: "error",
549
+ message: "No TIP token available. Login first with `/identity login`.",
550
+ };
551
+ }
552
+ identityToken = tip.token;
553
+ }
554
+ else {
555
+ const session = await getSessionWithRefresh(storeDir, sessionKey, deps.getOidcConfigForRefresh);
556
+ if (!session?.userToken) {
557
+ return {
558
+ kind: "error",
559
+ message: "No session found. Login first with `/identity login`.",
560
+ };
561
+ }
562
+ identityToken = session.userToken;
563
+ }
564
+ try {
565
+ const result = await identityClient.getRoleCredentials({
566
+ IdentityToken: identityToken,
567
+ ProviderName: params.providerName,
568
+ PoolName: deps.workloadPoolName,
569
+ });
570
+ logInfo(logger, `getRoleCredentials OK for provider=${params.providerName}`);
571
+ return {
572
+ kind: "success",
573
+ credentials: {
574
+ AccessKeyId: result.Credentials.AccessKeyId,
575
+ SecretAccessKey: result.Credentials.SecretAccessKey,
576
+ SessionToken: result.Credentials.SessionToken,
577
+ Expiration: result.Credentials.Expiration,
578
+ },
579
+ };
580
+ }
581
+ catch (err) {
582
+ logWarn(logger, `getRoleCredentials failed: ${String(err)}`);
583
+ return {
584
+ kind: "error",
585
+ message: `Failed to get role credentials: ${err.message}`,
586
+ };
587
+ }
588
+ }
589
+ /**
590
+ * Return the current TIP JWT for the session (refresh/obtain via user token if needed).
591
+ */
592
+ export async function runGetTipToken(deps, sessionKey, config) {
593
+ const tip = await resolveAndRefreshTIP(deps, sessionKey, config);
594
+ if (!tip) {
595
+ return {
596
+ kind: "error",
597
+ message: "No TIP token available. Log in with `/identity login` or ensure the session has a valid OIDC user token.",
598
+ };
599
+ }
600
+ return {
601
+ kind: "success",
602
+ tipToken: tip.token,
603
+ sub: tip.sub,
604
+ issuedAt: tip.issuedAt,
605
+ expiresAt: tip.expiresAt,
606
+ };
607
+ }
608
+ /**
609
+ * Return the OIDC id_token (user / session identity token) for the session.
610
+ */
611
+ export async function runGetSessionToken(deps, sessionKey) {
612
+ const session = await getSessionWithRefresh(deps.storeDir, sessionKey, deps.getOidcConfigForRefresh);
613
+ if (!session?.userToken) {
614
+ return {
615
+ kind: "error",
616
+ message: "No OIDC session. Log in with `/identity login` or use identity.session.put to inject a token.",
617
+ };
618
+ }
619
+ return {
620
+ kind: "success",
621
+ sessionIdToken: session.userToken,
622
+ sub: session.sub,
623
+ loginAt: session.loginAt,
624
+ expiresAt: session.expiresAt,
625
+ };
626
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"identity-commands.d.ts","sourceRoot":"","sources":["../../../src/commands/identity-commands.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAUL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,SAAS,EACf,MAAM,gCAAgC,CAAC;AAYxC,YAAY,EAAE,oBAAoB,EAAE,SAAS,EAAE,CAAC;AAEhD,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AAyoBvD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB;;;;;mBAlf3C,oBAAoB,KAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;EA2fpE;AAED,0CAA0C;AAC1C,wBAAgB,eAAe,CAAC,IAAI,EAAE,oBAAoB;;;;;mBA9frC,oBAAoB,KAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;EAugBpE"}
1
+ {"version":3,"file":"identity-commands.d.ts","sourceRoot":"","sources":["../../../src/commands/identity-commands.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAYL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,SAAS,EACf,MAAM,gCAAgC,CAAC;AAaxC,YAAY,EAAE,oBAAoB,EAAE,SAAS,EAAE,CAAC;AAEhD,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AAyxBvD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB;;;;;mBA3nB3C,oBAAoB,KAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;EAooBpE;AAED,0CAA0C;AAC1C,wBAAgB,eAAe,CAAC,IAAI,EAAE,oBAAoB;;;;;mBAvoBrC,oBAAoB,KAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;EAgpBpE"}
@@ -13,9 +13,10 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { runStatus, runLogin, runLogout, runListCredentials, runListTips, runConfig, runFetch, runSetBinding, runUnsetBinding, } from "../actions/identity-actions.js";
16
+ import { runStatus, runLogin, runLogout, runListCredentials, runListRoleCredentials, runGetRoleCredentials, runListTips, runConfig, runFetch, runSetBinding, runUnsetBinding, } from "../actions/identity-actions.js";
17
17
  import { deriveSessionKey, deriveDeliveryTargetFromContext, } from "../utils/derive-session-key.js";
18
18
  import { buildEffectiveSessionKey } from "../store/sender-session-store.js";
19
+ import { pluginState } from "../preflight/plugin-state.js";
19
20
  import { logDebug } from "../utils/logger.js";
20
21
  import { diagnoseRisk } from "../risk/diagnose-risk.js";
21
22
  import { getRiskPatterns } from "../risk/classify-risk.js";
@@ -27,10 +28,12 @@ Subcommands:
27
28
  • \`status\` – Full status: session/TIP details (issued, expires, chain), credentials, bindings
28
29
  • \`login\` – Log in via OIDC (returns auth URL if not logged in)
29
30
  • \`logout\` – Clear session and TIP
30
- • \`list-credentials\` or \`list [page]\` – List providers and credentials (paginated; shows bound env)
31
+ • \`list-credentials\` or \`list [page]\` – List credential providers (OAuth/API key) and stored credentials
32
+ • \`list-roles\` – List role credential providers (STS)
33
+ • \`get-role <provider> [--use-tip] [--show-secrets]\` – Get STS credentials for a role provider (credentials masked by default)
31
34
  • \`list-tips\` – List all valid TIP tokens with delegation chain and bindings
32
35
  • \`config\` – Show identity plugin configuration (redacted)
33
- • \`fetch <provider> [--flow=oauth2-user|oauth2-m2m|apikey] [--redirectUrl=<url>] [--scopes=a,b]\` – Add credential
36
+ • \`fetch <provider> [--flow=oauth2-user|oauth2-m2m|apikey|user] [--redirectUrl=<url>] [--scopes=a,b]\` – Add credential
34
37
  • \`set <provider> <envVar>\` – Bind credential to env var for tool injection
35
38
  • \`unset <provider>\` – Remove credential env binding
36
39
  • \`risk <command>\` – Diagnose risk for a shell command (e.g. exec)
@@ -38,7 +41,7 @@ Subcommands:
38
41
  • \`approve <approval_id>\` – Approve a pending high-risk tool call
39
42
  • \`reject <approval_id>\` – Reject a pending high-risk tool call
40
43
 
41
- Fetch: flow auto-inferred from control-plane provider type; override with \`--flow=oauth2-user|oauth2-m2m|apikey\`.
44
+ Fetch: flow auto-inferred from control-plane provider type; override with \`--flow=oauth2-user|oauth2-m2m|apikey|user\`.
42
45
 
43
46
  Examples:
44
47
  \`/identity\` – show this help
@@ -48,6 +51,8 @@ Examples:
48
51
  \`/identity fetch google\`
49
52
  \`/identity fetch openai --flow=apikey\`
50
53
  \`/identity list 2\` – load more (page 2)
54
+ \`/identity list-roles\` – list role credential providers (STS)
55
+ \`/identity get-role test_for_workload_token\` – get STS credentials
51
56
  \`/identity risk "rm -rf /"\` – check if command would require approval
52
57
  \`/identity risk-patterns\` – list risky patterns
53
58
  \`/identity approve abc123\` – approve pending tool call
@@ -88,7 +93,7 @@ function parseSubcommand(args) {
88
93
  rest: raw.slice(space + 1).trim(),
89
94
  };
90
95
  }
91
- /** Parse list args: optional page and filters. E.g. "list", "list 2", "list --name=github --flow=M2M". */
96
+ /** Parse list args: optional page and filters. E.g. "list", "list 2", "list --name=github --flow=M2M --type=api_key". */
92
97
  function parseListArgs(rest) {
93
98
  const parts = rest.split(/\s+/).filter(Boolean);
94
99
  const flags = {};
@@ -108,6 +113,7 @@ function parseListArgs(rest) {
108
113
  page: Number.isFinite(page) && page >= 1 ? page : 1,
109
114
  name: flags.name || undefined,
110
115
  flow: flags.flow || undefined,
116
+ type: flags.type || undefined,
111
117
  };
112
118
  }
113
119
  /** Parse fetch args: provider and flags (--flow, --redirectUrl, --scopes). flow can be omitted to auto-infer from provider. */
@@ -129,7 +135,7 @@ function parseFetchArgs(rest) {
129
135
  const provider = positional[0]?.trim();
130
136
  if (!provider) {
131
137
  return {
132
- error: "Usage: `/identity fetch <provider> [--flow=oauth2-user|oauth2-m2m|apikey] [--redirectUrl=<url>] [--scopes=a,b,c]`",
138
+ error: "Usage: `/identity fetch <provider> [--flow=oauth2-user|oauth2-m2m|apikey|user] [--redirectUrl=<url>] [--scopes=a,b,c]`",
133
139
  };
134
140
  }
135
141
  const flowRaw = flags.flow;
@@ -139,7 +145,9 @@ function parseFetchArgs(rest) {
139
145
  ? "apikey"
140
146
  : flowRaw === "oauth2-user"
141
147
  ? "oauth2-user"
142
- : undefined;
148
+ : flowRaw === "user"
149
+ ? "user"
150
+ : undefined;
143
151
  const redirectUrl = flags.redirectUrl?.trim() || undefined;
144
152
  const scopes = flags.scopes
145
153
  ? flags.scopes
@@ -179,6 +187,8 @@ function createIdentityHandler(deps) {
179
187
  "fetch",
180
188
  "list-credentials",
181
189
  "list",
190
+ "list-roles",
191
+ "get-role",
182
192
  "set",
183
193
  "unset",
184
194
  "approve",
@@ -204,6 +214,10 @@ function createIdentityHandler(deps) {
204
214
  case "list-credentials":
205
215
  case "list":
206
216
  return handleListCredentials(deps, sessionKey, rest);
217
+ case "list-roles":
218
+ return handleListRoles(deps, sessionKey, rest);
219
+ case "get-role":
220
+ return handleGetRoleCredentials(ctx, deps, sessionKey, rest);
207
221
  case "fetch":
208
222
  return handleFetch(ctx, deps, sessionKey, rest);
209
223
  case "set":
@@ -266,6 +280,16 @@ async function handleStatus(ctx, deps, sessionKey) {
266
280
  const result = await runStatus(deps, sessionKey, ctx.config);
267
281
  const lines = [];
268
282
  const now = Date.now();
283
+ // Show degraded banner when preflight detected config problems
284
+ if (pluginState.degraded && pluginState.failures.length > 0) {
285
+ lines.push("⚠️ **[DEGRADED] Plugin is running in degraded mode. Interception disabled.**");
286
+ lines.push("All login prompts and tool authorization checks are skipped until the following issues are resolved:");
287
+ lines.push("");
288
+ for (const f of pluginState.failures) {
289
+ lines.push(`• **${f.check}**: ${f.reason}`);
290
+ }
291
+ lines.push("");
292
+ }
269
293
  if (result.loggedIn) {
270
294
  lines.push(`✓ Logged in as \`${result.sub}\``);
271
295
  lines.push("");
@@ -334,13 +358,117 @@ async function handleLogout(deps, sessionKey) {
334
358
  await runLogout(deps, sessionKey);
335
359
  return { text: "✓ Logged out." };
336
360
  }
361
+ /** Parse list-roles args: optional name filter. E.g. "list-roles", "list-roles github", "list-roles --name=foo". */
362
+ function parseListRolesArgs(rest) {
363
+ const parts = rest.split(/\s+/).filter(Boolean);
364
+ const flags = {};
365
+ const positional = [];
366
+ for (const p of parts) {
367
+ if (p.startsWith("--")) {
368
+ const eq = p.indexOf("=");
369
+ if (eq > 2)
370
+ flags[p.slice(2, eq)] = p.slice(eq + 1);
371
+ }
372
+ else {
373
+ positional.push(p);
374
+ }
375
+ }
376
+ return {
377
+ name: flags.name || positional[0] || undefined,
378
+ };
379
+ }
380
+ async function handleListRoles(deps, sessionKey, rest) {
381
+ const { name } = parseListRolesArgs(rest);
382
+ const filter = name ? { name } : undefined;
383
+ const result = await runListRoleCredentials(deps, sessionKey, filter);
384
+ if (result.providers.length === 0) {
385
+ return {
386
+ text: "No role credential providers. Configure role providers in the control plane, or use `/identity list` for OAuth/API key providers.",
387
+ };
388
+ }
389
+ const lines = ["**Role credential providers (STS):**"];
390
+ for (const p of result.providers) {
391
+ const src = p.identitySource ? ` [${p.identitySource}]` : "";
392
+ lines.push(`• **${p.name}**${src}`);
393
+ }
394
+ lines.push("");
395
+ lines.push("Use `identity_get_role_credentials` tool to obtain STS credentials for a provider.");
396
+ return { text: lines.join("\n") };
397
+ }
398
+ /** Mask sensitive value: show prefix + suffix only. Returns full value when showSecrets is true. */
399
+ function maskSecret(value, showSecrets) {
400
+ if (showSecrets)
401
+ return value;
402
+ if (!value || value.length <= 8)
403
+ return "***";
404
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
405
+ }
406
+ /** Parse get-role args: provider name (required), optional --use-tip, --show-secrets. */
407
+ function parseGetRoleArgs(rest) {
408
+ const parts = rest.split(/\s+/).filter(Boolean);
409
+ const flags = {};
410
+ const positional = [];
411
+ for (const p of parts) {
412
+ if (p.startsWith("--")) {
413
+ const eq = p.indexOf("=");
414
+ if (eq > 2)
415
+ flags[p.slice(2, eq)] = p.slice(eq + 1);
416
+ else
417
+ flags[p.slice(2)] = "true";
418
+ }
419
+ else {
420
+ positional.push(p);
421
+ }
422
+ }
423
+ const providerName = positional[0]?.trim();
424
+ if (!providerName) {
425
+ return {
426
+ error: "Usage: `/identity get-role <provider> [--use-tip] [--show-secrets]`\nExample: `/identity get-role test_for_workload_token`",
427
+ };
428
+ }
429
+ const useTip = flags["use-tip"] === "true" || flags.useTip === "true";
430
+ const showSecrets = flags["show-secrets"] === "true" || flags.showSecrets === "true";
431
+ return { providerName, useTip, showSecrets };
432
+ }
433
+ async function handleGetRoleCredentials(ctx, deps, sessionKey, rest) {
434
+ const parsed = parseGetRoleArgs(rest);
435
+ if ("error" in parsed)
436
+ return { text: parsed.error };
437
+ const result = await runGetRoleCredentials(deps, sessionKey, {
438
+ providerName: parsed.providerName,
439
+ useTip: parsed.useTip,
440
+ config: ctx.config,
441
+ });
442
+ if (result.kind === "error") {
443
+ return { text: `⚠️ ${result.message}` };
444
+ }
445
+ const c = result.credentials;
446
+ const showSecrets = parsed.showSecrets;
447
+ const lines = [
448
+ `✓ STS credentials for \`${parsed.providerName}\``,
449
+ "",
450
+ "**Credentials:**",
451
+ `• AccessKeyId: \`${maskSecret(c.AccessKeyId, showSecrets)}\``,
452
+ `• SecretAccessKey: \`${maskSecret(c.SecretAccessKey, showSecrets)}\``,
453
+ `• SessionToken: \`${maskSecret(c.SessionToken, showSecrets)}\``,
454
+ ];
455
+ if (c.Expiration)
456
+ lines.push(`• Expiration: ${c.Expiration}`);
457
+ if (showSecrets) {
458
+ lines.push("", "⚠️ **Sensitive values are visible.** Avoid sharing this output.");
459
+ }
460
+ else {
461
+ lines.push("", "Use `--show-secrets` to reveal full values (use with caution).");
462
+ }
463
+ return { text: lines.join("\n") };
464
+ }
337
465
  async function handleListCredentials(deps, sessionKey, rest) {
338
- const { page, name, flow } = parseListArgs(rest);
339
- const filter = name || flow ? { name, flow } : undefined;
466
+ const { page, name, flow, type } = parseListArgs(rest);
467
+ const filter = name || flow || type ? { name, flow, type } : undefined;
340
468
  const result = await runListCredentials(deps, sessionKey, page, filter);
341
469
  const lines = [];
342
470
  if (result.providers.length > 0) {
343
- lines.push(`**Available (page ${result.page}):**`);
471
+ lines.push(`**Providers (page ${result.page}):**`);
344
472
  for (const p of result.providers) {
345
473
  const bind = p.binding ? ` → \`${p.binding}\`` : "";
346
474
  lines.push(`• **${p.name}** (${p.type}): ${p.status}${bind}`);
@@ -361,7 +489,7 @@ async function handleListCredentials(deps, sessionKey, rest) {
361
489
  };
362
490
  }
363
491
  const footer = [];
364
- footer.push("Use `/identity set <provider> <envVar>` to bind.");
492
+ footer.push("Use `/identity set <provider> <envVar>` to bind, or `/identity fetch <provider>` to add credentials.");
365
493
  if (result.hasMore) {
366
494
  footer.push(`\`/identity list ${result.page + 1}\` to load more.`);
367
495
  }