auramaxx 0.0.13 → 0.0.15

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 (117) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +37 -37
  3. package/.next/app-path-routes-manifest.json +8 -8
  4. package/.next/build-manifest.json +2 -2
  5. package/.next/prerender-manifest.json +34 -34
  6. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  7. package/.next/server/app/_not-found.html +1 -1
  8. package/.next/server/app/_not-found.rsc +1 -1
  9. package/.next/server/app/api/[...doc]/page.js.nft.json +1 -1
  10. package/.next/server/app/api/[...doc]/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/api/agent-requests/route_client-reference-manifest.js +1 -1
  12. package/.next/server/app/api/apps/install/route_client-reference-manifest.js +1 -1
  13. package/.next/server/app/api/apps/manifests/route_client-reference-manifest.js +1 -1
  14. package/.next/server/app/api/apps/static/[...path]/route_client-reference-manifest.js +1 -1
  15. package/.next/server/app/api/docs/plain/route.js.nft.json +1 -1
  16. package/.next/server/app/api/docs/plain/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/events/route_client-reference-manifest.js +1 -1
  18. package/.next/server/app/api/import-from-openclaw/[channel]/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/import-from-openclaw/route_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/import-from-openclaw/validate/[channel]/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/page.js.nft.json +1 -1
  22. package/.next/server/app/api/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
  24. package/.next/server/app/api/update/route.js +7 -8
  25. package/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/version/route.js +1 -1
  27. package/.next/server/app/api/version/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/workspace/[id]/apps/[wid]/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/workspace/[id]/apps/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/workspace/[id]/export/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/workspace/[id]/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/workspace/config/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/workspace/import/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/workspace/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/app-legacy-do-not-use/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/app-legacy-do-not-use.html +1 -1
  37. package/.next/server/app/app-legacy-do-not-use.rsc +1 -1
  38. package/.next/server/app/approve/[actionId]/page.js +1 -1
  39. package/.next/server/app/approve/[actionId]/page_client-reference-manifest.js +1 -1
  40. package/.next/server/app/docs/[...doc]/page.js.nft.json +1 -1
  41. package/.next/server/app/docs/[...doc]/page_client-reference-manifest.js +1 -1
  42. package/.next/server/app/docs/page.js.nft.json +1 -1
  43. package/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  44. package/.next/server/app/health/page_client-reference-manifest.js +1 -1
  45. package/.next/server/app/health.html +1 -1
  46. package/.next/server/app/health.rsc +1 -1
  47. package/.next/server/app/hello/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/hello.html +1 -1
  49. package/.next/server/app/hello.rsc +1 -1
  50. package/.next/server/app/index.html +1 -1
  51. package/.next/server/app/index.rsc +3 -3
  52. package/.next/server/app/page.js +2 -2
  53. package/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/server/app/privacy/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app/privacy.html +1 -1
  56. package/.next/server/app/privacy.rsc +1 -1
  57. package/.next/server/app/share/[token]/page_client-reference-manifest.js +1 -1
  58. package/.next/server/app/terms/page_client-reference-manifest.js +1 -1
  59. package/.next/server/app/terms.html +1 -1
  60. package/.next/server/app/terms.rsc +1 -1
  61. package/.next/server/app/yo/page.js +2 -2
  62. package/.next/server/app/yo/page_client-reference-manifest.js +1 -1
  63. package/.next/server/app/yo.html +1 -1
  64. package/.next/server/app/yo.rsc +3 -3
  65. package/.next/server/app-paths-manifest.json +8 -8
  66. package/.next/server/functions-config-manifest.json +2 -2
  67. package/.next/server/pages/404.html +1 -1
  68. package/.next/server/pages/500.html +1 -1
  69. package/.next/server/server-reference-manifest.json +1 -1
  70. package/.next/static/chunks/app/approve/[actionId]/page-c862645e19371cea.js +1 -0
  71. package/.next/static/chunks/app/{page-16dfcd1c7cc88bcc.js → page-9cd218b297b9c7bf.js} +1 -1
  72. package/.next/static/chunks/app/yo/page-fceb03605805cb44.js +1 -0
  73. package/.next/trace +28 -28
  74. package/README.md +1 -1
  75. package/docs/AGENT_SETUP.md +1 -1
  76. package/docs/AUTH.md +1 -1
  77. package/docs/CLI.md +0 -3
  78. package/docs/MCP.md +14 -7
  79. package/docs/SKILLS.md +1 -1
  80. package/docs/api/secrets/credentials.md +9 -0
  81. package/docs/external/HOW_TO_AURAMAXX/WORKING_WITH_SECRETS.md +2 -1
  82. package/docs/external/POLICY.md +2 -2
  83. package/package.json +1 -1
  84. package/public/opengraph.webp +0 -0
  85. package/skills/auramaxx/HEARTBEAT.md +3 -0
  86. package/skills/auramaxx/SKILL.md +13 -31
  87. package/skills/auramaxx/docs/AGENT_SETUP.md +1 -1
  88. package/src/app/UnlockPageClient.tsx +10 -10
  89. package/src/app/api/update/route.ts +9 -10
  90. package/src/app/approve/[actionId]/page.tsx +2 -1
  91. package/src/app/page.tsx +9 -0
  92. package/src/app/yo/layout.tsx +9 -0
  93. package/src/app/yo/page.tsx +1 -1
  94. package/src/components/layout/SettingsDrawer.tsx +1 -1
  95. package/src/server/cli/commands/agent.ts +75 -18
  96. package/src/server/cli/commands/init.ts +9 -9
  97. package/src/server/cli/commands/skill.ts +5 -2
  98. package/src/server/cli/lib/credential-resolve.ts +20 -1
  99. package/src/server/cli/lib/local-agent-trust.ts +4 -3
  100. package/src/server/lib/agent-profiles.ts +4 -3
  101. package/src/server/lib/update-check.ts +4 -0
  102. package/src/server/mcp/server.ts +88 -19
  103. package/src/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  104. package/src/server/routes/actions.ts +2 -1
  105. package/src/server/routes/credentials.ts +48 -5
  106. package/src/server/tests/cli/agent-auth.test.ts +190 -0
  107. package/src/server/tests/cli/local-agent-trust.test.ts +11 -6
  108. package/src/server/tests/endpoints/credentials.test.ts +40 -18
  109. package/src/server/tests/endpoints/escalation-migration-gate.test.ts +1 -1
  110. package/src/server/tests/lib/agent-profiles.test.ts +6 -0
  111. package/src/server/tests/lib/update-check.test.ts +9 -1
  112. package/src/server/tests/mcp/server.test.ts +142 -0
  113. package/src/server/tsconfig.tsbuildinfo +1 -1
  114. package/.next/static/chunks/app/approve/[actionId]/page-2acca1f490424f21.js +0 -1
  115. package/.next/static/chunks/app/yo/page-719dc5f213fdfb30.js +0 -1
  116. /package/.next/static/{WshFGr6RxGYP6AbWuT9OG → 7S943hv6A_w-7tx0a47LZ}/_buildManifest.js +0 -0
  117. /package/.next/static/{WshFGr6RxGYP6AbWuT9OG → 7S943hv6A_w-7tx0a47LZ}/_ssgManifest.js +0 -0
@@ -23,6 +23,15 @@ function hasClaimSecret(init: RequestInit | undefined, expected: string): boolea
23
23
  return false;
24
24
  }
25
25
 
26
+ function makeTestAgentToken(permissions: string[]): string {
27
+ const payload = Buffer.from(JSON.stringify({
28
+ agentId: 'cli-agent',
29
+ permissions,
30
+ exp: Date.now() + 60_000,
31
+ }), 'utf8').toString('base64url');
32
+ return `${payload}.sig`;
33
+ }
34
+
26
35
  const mocks = vi.hoisted(() => ({
27
36
  bootstrapViaSocket: vi.fn(),
28
37
  bootstrapViaAuthRequest: vi.fn(),
@@ -204,6 +213,82 @@ describe('agent CLI auth behavior', () => {
204
213
  expect(mocks.bootstrapViaAuthRequest).not.toHaveBeenCalled();
205
214
  });
206
215
 
216
+ it('inject honors --field by requesting and injecting that exact field', async () => {
217
+ mocks.bootstrapViaSocket.mockResolvedValueOnce('socket-token');
218
+ mocks.createReadToken.mockResolvedValueOnce('read-token');
219
+ mocks.decryptWithPrivateKey.mockImplementation((v: string) => v);
220
+
221
+ const readRequestBodies: Array<{ requestedFields?: string[] }> = [];
222
+
223
+ vi.spyOn(globalThis, 'fetch').mockImplementation(async (input: RequestInfo | URL, init?: RequestInit) => {
224
+ const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url);
225
+ const method = init?.method || 'GET';
226
+
227
+ if (url.pathname === '/credentials' && method === 'GET' && url.searchParams.get('q') === 'visa_7890') {
228
+ return new Response(JSON.stringify({
229
+ credentials: [{
230
+ id: 'cred-card',
231
+ name: 'visa_7890',
232
+ type: 'card',
233
+ agentId: 'primary',
234
+ meta: {},
235
+ }],
236
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
237
+ }
238
+
239
+ if (url.pathname === '/setup/agents' && method === 'GET') {
240
+ return new Response(JSON.stringify({
241
+ agents: [{ id: 'primary', name: 'primary', isPrimary: true }],
242
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
243
+ }
244
+
245
+ if (url.pathname === '/setup' && method === 'GET') {
246
+ return new Response(JSON.stringify({ projectScopeMode: 'auto' }), {
247
+ status: 200,
248
+ headers: { 'Content-Type': 'application/json' },
249
+ });
250
+ }
251
+
252
+ if (url.pathname === '/credentials/cred-card/read' && method === 'POST') {
253
+ if (typeof init?.body === 'string') {
254
+ readRequestBodies.push(JSON.parse(init.body) as { requestedFields?: string[] });
255
+ } else {
256
+ readRequestBodies.push({});
257
+ }
258
+ return new Response(JSON.stringify({
259
+ encrypted: JSON.stringify({
260
+ id: 'cred-card',
261
+ agentId: 'primary',
262
+ type: 'card',
263
+ fields: [
264
+ { key: 'number', value: '1234567890', sensitive: true },
265
+ { key: 'cvv', value: '123', sensitive: true },
266
+ ],
267
+ }),
268
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
269
+ }
270
+
271
+ throw new Error(`Unexpected fetch path: ${method} ${url.pathname}${url.search}`);
272
+ });
273
+
274
+ const exitCode = await runAgentCli([
275
+ 'inject',
276
+ 'visa_7890',
277
+ '--field',
278
+ 'cvv',
279
+ '--env',
280
+ 'VISA_CVV',
281
+ '--',
282
+ 'node',
283
+ '-e',
284
+ "process.exit(process.env.VISA_CVV === '123' ? 0 : 9)",
285
+ ]);
286
+
287
+ expect(exitCode).toBe(0);
288
+ expect(readRequestBodies).toHaveLength(1);
289
+ expect(readRequestBodies[0]?.requestedFields).toEqual(['cvv']);
290
+ });
291
+
207
292
  it('list supports --agent filter by name or id', async () => {
208
293
  mocks.bootstrapViaSocket.mockResolvedValue('socket-token');
209
294
 
@@ -1234,6 +1319,44 @@ describe('agent CLI auth behavior', () => {
1234
1319
  expect(getApprovalContext('req-rejected')).not.toBeNull();
1235
1320
  });
1236
1321
 
1322
+ it('auth claim maps 200 rejected status to claim_rejected and clears context', async () => {
1323
+ process.env.WALLET_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'auramaxx-agent-auth-'));
1324
+ putApprovalContext({
1325
+ reqId: 'req-rejected-status',
1326
+ secret: 'sec-rejected-status',
1327
+ privateKeyPem: 'mock-private-key',
1328
+ approvalScope: 'session_token',
1329
+ ttlSeconds: 300,
1330
+ retryCommandTemplate: 'npx auramaxx agent get "github" --reqId <reqId>',
1331
+ });
1332
+
1333
+ vi.spyOn(globalThis, 'fetch').mockImplementation(async (input: RequestInfo | URL, init?: RequestInit) => {
1334
+ const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url);
1335
+ const method = init?.method || 'GET';
1336
+ if (url.pathname === '/auth/req-rejected-status' && method === 'GET' && hasClaimSecret(init, 'sec-rejected-status')) {
1337
+ return new Response(JSON.stringify({ success: true, status: 'rejected' }), {
1338
+ status: 200,
1339
+ headers: { 'Content-Type': 'application/json' },
1340
+ });
1341
+ }
1342
+ throw new Error(`Unexpected fetch path: ${method} ${url.pathname}${url.search}`);
1343
+ });
1344
+
1345
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1346
+ const exitCode = await runAuthCli(['claim', 'req-rejected-status', '--json']);
1347
+ expect(exitCode).toBe(1);
1348
+
1349
+ const payload = JSON.parse(String(logSpy.mock.calls[0][0])) as {
1350
+ claimStatus?: string;
1351
+ errorCode?: string;
1352
+ retryReady?: boolean;
1353
+ };
1354
+ expect(payload.claimStatus).toBe('rejected');
1355
+ expect(payload.errorCode).toBe('claim_rejected');
1356
+ expect(payload.retryReady).toBe(false);
1357
+ expect(getApprovalContext('req-rejected-status')).toBeNull();
1358
+ });
1359
+
1237
1360
  it('auth request --help exits without creating an auth request', async () => {
1238
1361
  const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
1239
1362
  throw new Error('fetch should not run for --help');
@@ -1489,4 +1612,71 @@ describe('agent CLI auth behavior', () => {
1489
1612
  expect(mocks.createReadToken.mock.calls[0]?.[1]).toBe('fallback-auth-token');
1490
1613
  expect(logSpy).toHaveBeenCalledWith('enc(gh-secret)');
1491
1614
  });
1615
+
1616
+ it('skips delegated read token mint when fallback token is parseable non-admin', async () => {
1617
+ const nonAdminToken = makeTestAgentToken(['secret:read']);
1618
+ mocks.bootstrapViaSocket.mockRejectedValueOnce(new Error('Cannot connect to AuraMaxx: connect ENOENT /tmp/aura-cli.sock'));
1619
+ mocks.bootstrapViaAuthRequest.mockResolvedValueOnce(nonAdminToken);
1620
+ mocks.generateEphemeralKeypair.mockReturnValueOnce({
1621
+ publicKeyPem: 'ephemeral-public-key',
1622
+ privateKeyPem: 'ephemeral-private-key',
1623
+ publicKeyBase64: Buffer.from('ephemeral-public-key', 'utf8').toString('base64'),
1624
+ });
1625
+ mocks.decryptWithPrivateKey.mockImplementation((encrypted: string, privateKeyPem: string) => {
1626
+ expect(privateKeyPem).toBe('ephemeral-private-key');
1627
+ return encrypted;
1628
+ });
1629
+
1630
+ vi.spyOn(globalThis, 'fetch').mockImplementation(async (input: RequestInfo | URL, init?: RequestInit) => {
1631
+ const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url);
1632
+ const method = init?.method || 'GET';
1633
+
1634
+ if (url.pathname === '/credentials' && method === 'GET' && url.searchParams.get('q') === 'github') {
1635
+ return new Response(JSON.stringify({
1636
+ credentials: [{
1637
+ id: 'cred-github',
1638
+ name: 'github',
1639
+ type: 'note',
1640
+ agentId: 'primary',
1641
+ meta: {},
1642
+ }],
1643
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
1644
+ }
1645
+
1646
+ if (url.pathname === '/setup/agents' && method === 'GET') {
1647
+ return new Response(JSON.stringify({
1648
+ agents: [{ id: 'primary', name: 'primary', isPrimary: true }],
1649
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
1650
+ }
1651
+
1652
+ if (url.pathname === '/setup' && method === 'GET') {
1653
+ return new Response(JSON.stringify({ projectScopeMode: 'auto' }), {
1654
+ status: 200,
1655
+ headers: { 'Content-Type': 'application/json' },
1656
+ });
1657
+ }
1658
+
1659
+ if (url.pathname === '/credentials/cred-github/read' && method === 'POST') {
1660
+ const headers = init?.headers as Record<string, string>;
1661
+ expect(headers.Authorization).toBe(`Bearer ${nonAdminToken}`);
1662
+ return new Response(JSON.stringify({
1663
+ encrypted: JSON.stringify({
1664
+ id: 'cred-github',
1665
+ agentId: 'primary',
1666
+ type: 'note',
1667
+ fields: [{ key: 'value', value: 'gh-secret', sensitive: true }],
1668
+ }),
1669
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
1670
+ }
1671
+
1672
+ throw new Error(`Unexpected fetch path: ${method} ${url.pathname}${url.search}`);
1673
+ });
1674
+
1675
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1676
+ const exitCode = await runAgentCli(['get', 'github']);
1677
+
1678
+ expect(exitCode).toBe(0);
1679
+ expect(mocks.createReadToken).not.toHaveBeenCalled();
1680
+ expect(logSpy).toHaveBeenCalledWith('enc(gh-secret)');
1681
+ });
1492
1682
  });
@@ -19,15 +19,20 @@ describe('local agent trust helpers', () => {
19
19
  });
20
20
 
21
21
  it('resolves mode selection for numbered and named inputs', () => {
22
- expect(resolveLocalAgentModeChoice('')).toBe('dev');
23
- expect(resolveLocalAgentModeChoice('1')).toBe('dev');
22
+ expect(resolveLocalAgentModeChoice('')).toBe('admin');
23
+ expect(resolveLocalAgentModeChoice('1')).toBe('admin');
24
+ expect(resolveLocalAgentModeChoice('maxx')).toBe('admin');
25
+ expect(resolveLocalAgentModeChoice('work')).toBe('admin');
26
+ expect(resolveLocalAgentModeChoice('admin')).toBe('admin');
27
+
28
+ expect(resolveLocalAgentModeChoice('2')).toBe('dev');
29
+ expect(resolveLocalAgentModeChoice('mid')).toBe('dev');
24
30
  expect(resolveLocalAgentModeChoice('dev')).toBe('dev');
25
31
 
26
- expect(resolveLocalAgentModeChoice('2')).toBe('strict');
32
+ expect(resolveLocalAgentModeChoice('3')).toBe('strict');
33
+ expect(resolveLocalAgentModeChoice('sus')).toBe('strict');
34
+ expect(resolveLocalAgentModeChoice('local')).toBe('strict');
27
35
  expect(resolveLocalAgentModeChoice('strict')).toBe('strict');
28
-
29
- expect(resolveLocalAgentModeChoice('3')).toBe('admin');
30
- expect(resolveLocalAgentModeChoice('admin')).toBe('admin');
31
36
  });
32
37
 
33
38
  it('derives auto-approve defaults per profile mode', () => {
@@ -329,7 +329,7 @@ describe('Credential Endpoints', () => {
329
329
  expect(getDeletedRes.status).toBe(404);
330
330
  });
331
331
 
332
- it('should request human approval when default excluded fields are present in credential reads', async () => {
332
+ it('should only request human approval when excluded fields are explicitly requested', async () => {
333
333
  const { publicKey, privateKey } = generateKeyPairSync('rsa', {
334
334
  modulusLength: 2048,
335
335
  publicKeyEncoding: { type: 'spki', format: 'pem' },
@@ -391,21 +391,39 @@ describe('Credential Endpoints', () => {
391
391
  .post(`/credentials/${loginId}/read`)
392
392
  .set('Authorization', `Bearer ${token}`)
393
393
  .send({});
394
- expect(loginRead.status).toBe(403);
395
- expect(loginRead.body.reasonCode).toBe('DENY_EXCLUDED_FIELD');
396
- expect(loginRead.body.requiresHumanApproval).toBe(true);
397
- const loginReqId = (loginRead.body.reqId ?? loginRead.body.requestId) as string | undefined;
394
+ expect(loginRead.status).toBe(200);
395
+ const loginDecrypted = decryptCredentialPayload(loginRead.body.encrypted, privateKey);
396
+ expect(loginDecrypted.fields.map((field) => field.key)).not.toContain('password');
397
+ expect(loginDecrypted.fields.map((field) => field.key)).toContain('notes');
398
+
399
+ const cardRead = await request(app)
400
+ .post(`/credentials/${cardId}/read`)
401
+ .set('Authorization', `Bearer ${token}`)
402
+ .send({});
403
+ expect(cardRead.status).toBe(200);
404
+ const cardDecrypted = decryptCredentialPayload(cardRead.body.encrypted, privateKey);
405
+ expect(cardDecrypted.fields.map((field) => field.key)).toContain('number');
406
+ expect(cardDecrypted.fields.map((field) => field.key)).not.toContain('cvv');
407
+
408
+ const loginExplicitRead = await request(app)
409
+ .post(`/credentials/${loginId}/read`)
410
+ .set('Authorization', `Bearer ${token}`)
411
+ .send({ requestedFields: ['password'] });
412
+ expect(loginExplicitRead.status).toBe(403);
413
+ expect(loginExplicitRead.body.reasonCode).toBe('DENY_EXCLUDED_FIELD');
414
+ expect(loginExplicitRead.body.requiresHumanApproval).toBe(true);
415
+ const loginReqId = (loginExplicitRead.body.reqId ?? loginExplicitRead.body.requestId) as string | undefined;
398
416
  expect(typeof loginReqId).toBe('string');
399
- expect(typeof loginRead.body.secret).toBe('string');
417
+ expect(typeof loginExplicitRead.body.secret).toBe('string');
400
418
  const expectedPollUrl = `http://127.0.0.1:4242/auth/${encodeURIComponent(loginReqId as string)}`;
401
- expect(loginRead.body.pollUrl).toBe(expectedPollUrl);
402
- expect(loginRead.body.claim).toMatchObject({
419
+ expect(loginExplicitRead.body.pollUrl).toBe(expectedPollUrl);
420
+ expect(loginExplicitRead.body.claim).toMatchObject({
403
421
  method: 'GET',
404
422
  endpoint: `/auth/${encodeURIComponent(loginReqId as string)}`,
405
423
  });
406
- expect(loginRead.body.approvalFlow?.mode).toBe('one_time_scoped_read');
407
- expect(loginRead.body.requestedFields).toEqual(expect.arrayContaining(['password']));
408
- expect(loginRead.body.effectiveExcludeFields).toEqual(expect.arrayContaining(['password']));
424
+ expect(loginExplicitRead.body.approvalFlow?.mode).toBe('one_time_scoped_read');
425
+ expect(loginExplicitRead.body.requestedFields).toEqual(expect.arrayContaining(['password']));
426
+ expect(loginExplicitRead.body.effectiveExcludeFields).toEqual(expect.arrayContaining(['password']));
409
427
 
410
428
  const loginApproval = await testPrisma.humanAction.findUnique({
411
429
  where: { id: loginReqId as string },
@@ -426,13 +444,13 @@ describe('Credential Endpoints', () => {
426
444
  expect(loginApprovalMeta.credentialAccess?.maxReads).toBe(1);
427
445
  expect(loginApprovalMeta.credentialAccess?.ttl).toBe(300);
428
446
 
429
- const cardRead = await request(app)
447
+ const cardExplicitRead = await request(app)
430
448
  .post(`/credentials/${cardId}/read`)
431
449
  .set('Authorization', `Bearer ${token}`)
432
- .send({});
433
- expect(cardRead.status).toBe(403);
434
- expect(cardRead.body.reasonCode).toBe('DENY_EXCLUDED_FIELD');
435
- expect(cardRead.body.requestedFields).toEqual(expect.arrayContaining(['cvv']));
450
+ .send({ requestedFields: ['cvv'] });
451
+ expect(cardExplicitRead.status).toBe(403);
452
+ expect(cardExplicitRead.body.reasonCode).toBe('DENY_EXCLUDED_FIELD');
453
+ expect(cardExplicitRead.body.requestedFields).toEqual(expect.arrayContaining(['cvv']));
436
454
  });
437
455
 
438
456
  it('should issue scoped temporary read access after excluded-field approval', async () => {
@@ -487,7 +505,7 @@ describe('Credential Endpoints', () => {
487
505
  const deniedRead = await request(app)
488
506
  .post(`/credentials/${credentialId}/read`)
489
507
  .set('Authorization', `Bearer ${token}`)
490
- .send({});
508
+ .send({ requestedFields: ['password'] });
491
509
  expect(deniedRead.status).toBe(403);
492
510
  expect(deniedRead.body.reasonCode).toBe('DENY_EXCLUDED_FIELD');
493
511
  expect(deniedRead.body.routeId).toBe(ESCALATION_ROUTE_IDS.CREDENTIALS_READ_EXCLUDED_FIELD);
@@ -594,6 +612,7 @@ describe('Credential Endpoints', () => {
594
612
  .post(`/credentials/${credentialId}/read`)
595
613
  .set('Authorization', `Bearer ${token}`)
596
614
  .send({
615
+ requestedFields: ['password'],
597
616
  requestedPolicySource: 'derived_403',
598
617
  requestedPolicy: {
599
618
  permissions: ['admin:*'],
@@ -642,6 +661,7 @@ describe('Credential Endpoints', () => {
642
661
  .post(`/credentials/${credentialId}/read`)
643
662
  .set('Authorization', `Bearer ${token}`)
644
663
  .send({
664
+ requestedFields: ['password'],
645
665
  requestedPolicySource: 'derived_403',
646
666
  requestedPolicy: 'bad-value',
647
667
  });
@@ -688,6 +708,7 @@ describe('Credential Endpoints', () => {
688
708
  .post(`/credentials/${credentialId}/read`)
689
709
  .set('Authorization', `Bearer ${token}`)
690
710
  .send({
711
+ requestedFields: ['password'],
691
712
  requestedPolicySource: 'agent',
692
713
  requestedPolicy: {
693
714
  permissions: ['secret:read'],
@@ -711,6 +732,7 @@ describe('Credential Endpoints', () => {
711
732
  .post(`/credentials/${credentialId}/read`)
712
733
  .set('Authorization', `Bearer ${token}`)
713
734
  .send({
735
+ requestedFields: ['password'],
714
736
  requestedPolicySource: 'agent',
715
737
  requestedPolicy: {
716
738
  permissions: ['admin:*'],
@@ -864,7 +886,7 @@ describe('Credential Endpoints', () => {
864
886
  const deniedRead = await request(app)
865
887
  .post(`/credentials/${credentialId}/read`)
866
888
  .set('Authorization', `Bearer ${token}`)
867
- .send({});
889
+ .send({ requestedFields: ['password'] });
868
890
  expect(deniedRead.status).toBe(403);
869
891
  expect(deniedRead.body.reasonCode).toBe('DENY_EXCLUDED_FIELD');
870
892
  expect(deniedRead.body.routeId).toBe(ESCALATION_ROUTE_IDS.CREDENTIALS_READ_EXCLUDED_FIELD);
@@ -226,7 +226,7 @@ describe('Escalation rollout migration gates', () => {
226
226
  const readExcludedFieldRes = await request(app)
227
227
  .post(`/credentials/${credentialId}/read`)
228
228
  .set('Authorization', `Bearer ${readExcludedFieldToken}`)
229
- .send({});
229
+ .send({ requestedFields: ['password'] });
230
230
 
231
231
  expect(readExcludedFieldRes.status).toBe(403);
232
232
  expect(readExcludedFieldRes.body.contractVersion).toBe(ESCALATION_CONTRACT_VERSION);
@@ -53,6 +53,12 @@ describe('agent profiles resolver', () => {
53
53
  expect(policy.credentialAccess.maxReads).toBe(100);
54
54
  expect(policy.overrideDelta).toEqual(['maxReads', 'ttlSeconds']);
55
55
  });
56
+
57
+ it('scopes dev profile credential access to primary + legacy alias only', () => {
58
+ const policy = resolveProfileToEffectivePolicy({ profileId: 'dev' });
59
+ expect(policy.credentialAccess.read).toEqual(['agent:agent', 'agent:primary']);
60
+ expect(policy.credentialAccess.write).toEqual(['agent:agent', 'agent:primary']);
61
+ });
56
62
  });
57
63
 
58
64
  describe('describeProfile', () => {
@@ -1,5 +1,12 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { buildNpxLatestCommand, buildUpdateCommand, buildUpdateFallbackCommand, buildVersionInfo, isNewerVersion } from '../../lib/update-check';
2
+ import {
3
+ buildNpxLatestCommand,
4
+ buildUpdateCommand,
5
+ buildUpdateFallbackCommand,
6
+ buildUpdateForceCommand,
7
+ buildVersionInfo,
8
+ isNewerVersion,
9
+ } from '../../lib/update-check';
3
10
 
4
11
  describe('update-check helpers', () => {
5
12
  it('detects newer versions', () => {
@@ -12,6 +19,7 @@ describe('update-check helpers', () => {
12
19
  const info = buildVersionInfo('1.0.0', '1.1.0');
13
20
  expect(info.updateAvailable).toBe(true);
14
21
  expect(buildUpdateCommand()).toBe('npm install -g auramaxx --foreground-scripts');
22
+ expect(buildUpdateForceCommand()).toBe('npm install -g auramaxx --foreground-scripts --force');
15
23
  expect(buildNpxLatestCommand()).toBe('npx --yes auramaxx@latest');
16
24
  expect(buildNpxLatestCommand('auramaxx', ['restart'])).toBe('npx --yes auramaxx@latest restart');
17
25
  expect(buildUpdateFallbackCommand()).toBe('npx --yes auramaxx@latest start');
@@ -2844,6 +2844,148 @@ describe('MCP Server — Tool Registration', () => {
2844
2844
  vi.doUnmock('@modelcontextprotocol/sdk/server/stdio.js');
2845
2845
  });
2846
2846
 
2847
+ it('get_secret and inject_secret should enforce explicit field reads without primary fallback', async () => {
2848
+ const capturedHandlers: Array<{
2849
+ name: string;
2850
+ handler: (input: unknown) => Promise<unknown>;
2851
+ }> = [];
2852
+
2853
+ vi.doMock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
2854
+ McpServer: class {
2855
+ tool(name: string, _desc: string, _shape: unknown, handler: (input: unknown) => Promise<unknown>) {
2856
+ capturedHandlers.push({ name, handler });
2857
+ }
2858
+ resource() {}
2859
+ async connect() {}
2860
+ },
2861
+ }));
2862
+ vi.doMock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
2863
+ StdioServerTransport: class {},
2864
+ }));
2865
+ vi.doMock('crypto', async () => {
2866
+ const actual = await vi.importActual<typeof import('crypto')>('crypto');
2867
+ return {
2868
+ ...actual,
2869
+ privateDecrypt: vi.fn((_opts: unknown, ciphertext: Buffer) => {
2870
+ const marker = ciphertext.toString('utf8');
2871
+ if (marker === 'cred-cipher') {
2872
+ return Buffer.from(JSON.stringify({
2873
+ id: 'cred-1',
2874
+ agentId: 'primary',
2875
+ type: 'card',
2876
+ fields: [
2877
+ { key: 'number', value: '1234567890', sensitive: true },
2878
+ { key: 'cvv', value: '321', sensitive: true },
2879
+ ],
2880
+ }), 'utf8');
2881
+ }
2882
+ throw new Error(`Unexpected privateDecrypt input: ${marker}`);
2883
+ }),
2884
+ };
2885
+ });
2886
+
2887
+ vi.resetModules();
2888
+ const originalFetch = globalThis.fetch;
2889
+ const originalToken = process.env.AURA_TOKEN;
2890
+ const originalGetEnv = process.env.AURA_VISA_7890;
2891
+ const originalInjectEnv = process.env.MY_SECRET;
2892
+ process.env.AURA_TOKEN = 'base-token';
2893
+
2894
+ const readRequestBodies: Array<{ requestedFields?: string[] }> = [];
2895
+ const credentialCipher = Buffer.from('cred-cipher', 'utf8').toString('base64');
2896
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
2897
+ const url = String(input);
2898
+ const method = init?.method || 'GET';
2899
+
2900
+ if (url.endsWith('/setup') && method === 'GET') {
2901
+ return new Response(JSON.stringify({ projectScopeMode: 'auto' }), { status: 200 });
2902
+ }
2903
+
2904
+ if (url.endsWith('/setup/agents') && method === 'GET') {
2905
+ return new Response(JSON.stringify({ agents: [{ id: 'primary', name: 'primary' }] }), { status: 200 });
2906
+ }
2907
+
2908
+ if (url.includes('/credentials?q=visa_7890') && method === 'GET') {
2909
+ return new Response(JSON.stringify({
2910
+ credentials: [{ id: 'cred-1', name: 'visa_7890', type: 'card', agentId: 'primary' }],
2911
+ }), { status: 200 });
2912
+ }
2913
+
2914
+ if (url.endsWith('/credentials/cred-1/read') && method === 'POST') {
2915
+ if (init?.body) {
2916
+ readRequestBodies.push(JSON.parse(String(init.body)) as { requestedFields?: string[] });
2917
+ } else {
2918
+ readRequestBodies.push({});
2919
+ }
2920
+ return new Response(JSON.stringify({ encrypted: credentialCipher }), { status: 200 });
2921
+ }
2922
+
2923
+ return new Response('not mocked', { status: 500 });
2924
+ });
2925
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
2926
+
2927
+ await import('../../mcp/server.js');
2928
+ const getSecret = capturedHandlers.find((tool) => tool.name === 'get_secret');
2929
+ const injectSecret = capturedHandlers.find((tool) => tool.name === 'inject_secret');
2930
+ expect(getSecret).toBeDefined();
2931
+ expect(injectSecret).toBeDefined();
2932
+
2933
+ const getCvv = await getSecret!.handler({
2934
+ name: 'visa_7890',
2935
+ field: 'cvv',
2936
+ dangerPlaintext: true,
2937
+ }) as { content: Array<{ text: string }> };
2938
+ const getCvvPayload = JSON.parse(getCvv.content[0].text) as { secret?: string };
2939
+ expect(getCvvPayload.secret).toBe('321');
2940
+ expect(process.env.AURA_VISA_7890).toBe('321');
2941
+
2942
+ const getMissing = await getSecret!.handler({
2943
+ name: 'visa_7890',
2944
+ field: 'security_code',
2945
+ }) as { content: Array<{ text: string }> };
2946
+ const getMissingPayload = JSON.parse(getMissing.content[0].text) as { error?: string; availableFields?: string };
2947
+ expect(getMissingPayload.error).toContain('Field "security_code" not found on credential "visa_7890"');
2948
+ expect(getMissingPayload.availableFields).toContain('number');
2949
+ expect(getMissingPayload.availableFields).toContain('cvv');
2950
+
2951
+ const injectCvv = await injectSecret!.handler({
2952
+ name: 'visa_7890',
2953
+ field: 'cvv',
2954
+ envVar: 'MY_SECRET',
2955
+ dangerPlaintext: true,
2956
+ }) as { content: Array<{ text: string }> };
2957
+ const injectCvvPayload = JSON.parse(injectCvv.content[0].text) as { secret?: string };
2958
+ expect(injectCvvPayload.secret).toBe('321');
2959
+ expect(process.env.MY_SECRET).toBe('321');
2960
+
2961
+ const injectMissing = await injectSecret!.handler({
2962
+ name: 'visa_7890',
2963
+ field: 'security_code',
2964
+ envVar: 'MY_SECRET',
2965
+ }) as { content: Array<{ text: string }> };
2966
+ const injectMissingPayload = JSON.parse(injectMissing.content[0].text) as { error?: string; availableFields?: string };
2967
+ expect(injectMissingPayload.error).toContain('Field "security_code" not found on credential "visa_7890"');
2968
+ expect(injectMissingPayload.availableFields).toContain('number');
2969
+ expect(injectMissingPayload.availableFields).toContain('cvv');
2970
+
2971
+ expect(readRequestBodies).toEqual([
2972
+ { requestedFields: ['cvv'] },
2973
+ { requestedFields: ['security_code'] },
2974
+ { requestedFields: ['cvv'] },
2975
+ { requestedFields: ['security_code'] },
2976
+ ]);
2977
+
2978
+ if (originalGetEnv === undefined) delete process.env.AURA_VISA_7890;
2979
+ else process.env.AURA_VISA_7890 = originalGetEnv;
2980
+ if (originalInjectEnv === undefined) delete process.env.MY_SECRET;
2981
+ else process.env.MY_SECRET = originalInjectEnv;
2982
+ globalThis.fetch = originalFetch;
2983
+ process.env.AURA_TOKEN = originalToken;
2984
+ vi.doUnmock('crypto');
2985
+ vi.doUnmock('@modelcontextprotocol/sdk/server/mcp.js');
2986
+ vi.doUnmock('@modelcontextprotocol/sdk/server/stdio.js');
2987
+ });
2988
+
2847
2989
  it('write_diary should create a new diary note and fall back to primary when daily-logs is absent', async () => {
2848
2990
  const capturedHandlers: Array<{
2849
2991
  name: string;