@topolo/sdk 0.4.9 → 0.5.0

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.
@@ -16,20 +16,20 @@ function stubFetch(responses: Array<{ status?: number; body?: unknown; contentTy
16
16
  }
17
17
 
18
18
  const AGENT = { clientName: 'topolo-test', clientVersion: '0.0.0' };
19
- const SERVICE_LOOKUP = { body: { success: true, data: { service_id: 'service_test' } } };
19
+ const SERVICE_LOOKUP = { body: { success: true, data: { app_id: 'app_test' } } };
20
20
  const MAIL_CATALOG = {
21
21
  body: {
22
22
  data: {
23
23
  services: [
24
24
  {
25
- service_id: 'srv_mail',
25
+ app_id: 'app_mail',
26
26
  app_slug: 'topolo-mail',
27
27
  name: 'Topolo Mail',
28
28
  api_base_url: 'https://mail-api.topolo.app',
29
29
  launch_url: 'https://mail.topolo.app',
30
30
  surface_kind: 'api',
31
31
  status: 'active',
32
- granted_permissions: ['srv_mail.messages:read'],
32
+ granted_permissions: ['app_mail.messages:read'],
33
33
  },
34
34
  ],
35
35
  },
@@ -40,18 +40,18 @@ const ACTION_CATALOG = {
40
40
  data: {
41
41
  actions: [
42
42
  {
43
- action_id: 'srv_mail.messages.list',
43
+ action_id: 'app_mail.messages.list',
44
44
  name: 'messages.list',
45
45
  tool_name: 'topolo_mail_messages_list',
46
46
  title: 'List messages',
47
47
  description: 'List Mail messages',
48
- service_id: 'srv_mail',
48
+ app_id: 'app_mail',
49
49
  service_slug: 'topolo-mail',
50
50
  service_name: 'Topolo Mail',
51
51
  method: 'GET',
52
52
  path: '/api/mail/mailboxes/{mailboxId}/messages',
53
53
  permission_name: 'messages:read',
54
- required_permission: 'srv_mail.messages:read',
54
+ required_permission: 'app_mail.messages:read',
55
55
  input_schema: { type: 'object' },
56
56
  output_schema: {},
57
57
  read_only: true,
@@ -59,18 +59,18 @@ const ACTION_CATALOG = {
59
59
  requires_confirmation: false,
60
60
  },
61
61
  {
62
- action_id: 'srv_mail.messages.get',
62
+ action_id: 'app_mail.messages.get',
63
63
  name: 'messages.get',
64
64
  tool_name: 'topolo_mail_messages_get',
65
65
  title: 'Get message',
66
66
  description: 'Get Mail message',
67
- service_id: 'srv_mail',
67
+ app_id: 'app_mail',
68
68
  service_slug: 'topolo-mail',
69
69
  service_name: 'Topolo Mail',
70
70
  method: 'GET',
71
71
  path: '/api/mail/mailboxes/{mailboxId}/messages/{messageId}',
72
72
  permission_name: 'messages:read',
73
- required_permission: 'srv_mail.messages:read',
73
+ required_permission: 'app_mail.messages:read',
74
74
  input_schema: { type: 'object' },
75
75
  output_schema: {},
76
76
  read_only: true,
@@ -78,18 +78,18 @@ const ACTION_CATALOG = {
78
78
  requires_confirmation: false,
79
79
  },
80
80
  {
81
- action_id: 'srv_mail.messages.send',
81
+ action_id: 'app_mail.messages.send',
82
82
  name: 'messages.send',
83
83
  tool_name: 'topolo_mail_messages_send',
84
84
  title: 'Send message',
85
85
  description: 'Send Mail message',
86
- service_id: 'srv_mail',
86
+ app_id: 'app_mail',
87
87
  service_slug: 'topolo-mail',
88
88
  service_name: 'Topolo Mail',
89
89
  method: 'POST',
90
90
  path: '/api/mail/mailboxes/{mailboxId}/messages',
91
91
  permission_name: 'messages:send',
92
- required_permission: 'srv_mail.messages:send',
92
+ required_permission: 'app_mail.messages:send',
93
93
  input_schema: { type: 'object' },
94
94
  output_schema: {},
95
95
  read_only: false,
@@ -110,7 +110,7 @@ function requestCalls(calls: Array<{ url: string; init: RequestInit }>) {
110
110
  }
111
111
 
112
112
  beforeEach(() => {
113
- TopoloClient.clearServiceIdCache();
113
+ TopoloClient.clearAppIdCache();
114
114
  });
115
115
 
116
116
  describe('TopoloClient.request', () => {
@@ -120,12 +120,12 @@ describe('TopoloClient.request', () => {
120
120
  credential: { kind: 'access_token', accessToken: 'tok_abc' },
121
121
  agent: AGENT,
122
122
  fetch: impl,
123
- serviceIds: { auth: 'service_auth' },
123
+ appIds: { auth: 'service_auth' },
124
124
  });
125
125
  await client.request({ service: 'auth', path: '/api/me' });
126
126
  const headers = new Headers(requestCalls(calls)[0]!.init.headers);
127
127
  expect(headers.get('Authorization')).toBe('Bearer tok_abc');
128
- expect(headers.get('X-Service-ID')).toBe('service_auth');
128
+ expect(headers.get('X-App-ID')).toBe('service_auth');
129
129
  expect(headers.get('X-Topolo-Client')).toBe('topolo-test/0.0.0');
130
130
  expect(headers.get('X-Topolo-Request-Id')).toMatch(/.+/);
131
131
  });
@@ -384,9 +384,9 @@ describe('TopoloClient.debug hook', () => {
384
384
  });
385
385
  });
386
386
 
387
- describe('TopoloClient.getServiceIdBySlug', () => {
387
+ describe('TopoloClient.getAppIdBySlug', () => {
388
388
  function freshClient(impl: typeof fetch, serviceUrls?: Record<string, string>) {
389
- TopoloClient.clearServiceIdCache();
389
+ TopoloClient.clearAppIdCache();
390
390
  return new TopoloClient({
391
391
  credential: { kind: 'api_key', apiKey: 'x' },
392
392
  agent: AGENT,
@@ -402,19 +402,19 @@ describe('TopoloClient.getServiceIdBySlug', () => {
402
402
  agent: AGENT,
403
403
  fetch: impl,
404
404
  serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' } as never,
405
- serviceIds: { notify: 'service_notify' },
405
+ appIds: { notify: 'service_notify' },
406
406
  });
407
- const id = await client.getServiceIdBySlug('topolo-notify');
407
+ const id = await client.getAppIdBySlug('topolo-notify');
408
408
  expect(id).toBe('service_notify');
409
409
  expect(calls).toEqual([]);
410
410
  });
411
411
 
412
412
  it('falls back to /api/services/by-slug/:slug for unknown slugs', async () => {
413
413
  const { impl, calls } = stubFetch([
414
- { body: { success: true, data: { service_id: 'service_abc123', api_base_url: 'https://x-api' } } },
414
+ { body: { success: true, data: { app_id: 'service_abc123', api_base_url: 'https://x-api' } } },
415
415
  ]);
416
416
  const client = freshClient(impl, { auth: 'https://auth.example.com', developers: 'https://auth.example.com' });
417
- const id = await client.getServiceIdBySlug('external-service');
417
+ const id = await client.getAppIdBySlug('external-service');
418
418
  expect(id).toBe('service_abc123');
419
419
  expect(calls[0]!.url).toBe('https://auth.example.com/api/services/by-slug/external-service');
420
420
  // No auth header — endpoint is public.
@@ -426,27 +426,27 @@ describe('TopoloClient.getServiceIdBySlug', () => {
426
426
  it('returns null on 404 and caches the negative result', async () => {
427
427
  const { impl, calls } = stubFetch([{ status: 404, body: { success: false } }]);
428
428
  const client = freshClient(impl, { auth: 'https://auth.example.com', developers: 'https://auth.example.com' });
429
- expect(await client.getServiceIdBySlug('nope')).toBeNull();
430
- expect(await client.getServiceIdBySlug('nope')).toBeNull();
429
+ expect(await client.getAppIdBySlug('nope')).toBeNull();
430
+ expect(await client.getAppIdBySlug('nope')).toBeNull();
431
431
  expect(calls.length).toBe(1);
432
432
  });
433
433
 
434
434
  it('caches positive results across calls', async () => {
435
435
  const { impl, calls } = stubFetch([
436
- { body: { success: true, data: { service_id: 'service_external' } } },
436
+ { body: { success: true, data: { app_id: 'service_external' } } },
437
437
  ]);
438
438
  const client = freshClient(impl, { auth: 'https://auth.example.com', developers: 'https://auth.example.com' });
439
- expect(await client.getServiceIdBySlug('external-service')).toBe('service_external');
440
- expect(await client.getServiceIdBySlug('external-service')).toBe('service_external');
439
+ expect(await client.getAppIdBySlug('external-service')).toBe('service_external');
440
+ expect(await client.getAppIdBySlug('external-service')).toBe('service_external');
441
441
  expect(calls.length).toBe(1);
442
442
  });
443
443
 
444
444
  it('normalizes slug case + whitespace before hitting the network', async () => {
445
445
  const { impl, calls } = stubFetch([
446
- { body: { success: true, data: { service_id: 'service_x' } } },
446
+ { body: { success: true, data: { app_id: 'service_x' } } },
447
447
  ]);
448
448
  const client = freshClient(impl, { auth: 'https://auth.example.com', developers: 'https://auth.example.com' });
449
- await client.getServiceIdBySlug(' EXTERNAL-SERVICE ');
449
+ await client.getAppIdBySlug(' EXTERNAL-SERVICE ');
450
450
  expect(calls[0]!.url).toBe('https://auth.example.com/api/services/by-slug/external-service');
451
451
  });
452
452
 
@@ -457,21 +457,21 @@ describe('TopoloClient.getServiceIdBySlug', () => {
457
457
  throws = false;
458
458
  throw new Error('ECONNREFUSED');
459
459
  }
460
- return new Response(JSON.stringify({ success: true, data: { service_id: 'service_y' } }), {
460
+ return new Response(JSON.stringify({ success: true, data: { app_id: 'service_y' } }), {
461
461
  status: 200,
462
462
  headers: { 'Content-Type': 'application/json' },
463
463
  });
464
464
  }) as unknown as typeof fetch;
465
- TopoloClient.clearServiceIdCache();
465
+ TopoloClient.clearAppIdCache();
466
466
  const client = new TopoloClient({
467
467
  credential: { kind: 'api_key', apiKey: 'x' },
468
468
  agent: AGENT,
469
469
  fetch: impl,
470
470
  serviceUrls: { auth: 'https://auth.example.com', developers: 'https://auth.example.com' } as never,
471
471
  });
472
- expect(await client.getServiceIdBySlug('flaky')).toBeNull();
472
+ expect(await client.getAppIdBySlug('flaky')).toBeNull();
473
473
  // Second call retries — earlier null was not cached.
474
- expect(await client.getServiceIdBySlug('flaky')).toBe('service_y');
474
+ expect(await client.getAppIdBySlug('flaky')).toBe('service_y');
475
475
  });
476
476
  });
477
477
 
@@ -483,7 +483,7 @@ describe('TopoloClient.listServices', () => {
483
483
  data: {
484
484
  services: [
485
485
  {
486
- service_id: 'srv_wRCnLLbtvmVT',
486
+ app_id: 'app_topolo_director',
487
487
  app_slug: 'topolo-director',
488
488
  name: 'Topolo Director',
489
489
  description: 'Demo control plane',
@@ -512,7 +512,7 @@ describe('TopoloClient.listServices', () => {
512
512
  exceptionPolicy: 'app_or_organization',
513
513
  },
514
514
  ],
515
- granted_permissions: ['srv_wRCnLLbtvmVT.dashboard:read'],
515
+ granted_permissions: ['app_topolo_director.dashboard:read'],
516
516
  },
517
517
  ],
518
518
  },
@@ -528,7 +528,7 @@ describe('TopoloClient.listServices', () => {
528
528
  const out = await client.listServices();
529
529
  expect(out).toEqual([
530
530
  {
531
- serviceId: 'srv_wRCnLLbtvmVT',
531
+ appId: 'app_topolo_director',
532
532
  slug: 'topolo-director',
533
533
  name: 'Topolo Director',
534
534
  description: 'Demo control plane',
@@ -557,7 +557,7 @@ describe('TopoloClient.listServices', () => {
557
557
  exceptionPolicy: 'app_or_organization',
558
558
  },
559
559
  ],
560
- permissions: ['srv_wRCnLLbtvmVT.dashboard:read'],
560
+ permissions: ['app_topolo_director.dashboard:read'],
561
561
  },
562
562
  ]);
563
563
  expect(calls[0]!.url).toBe(
@@ -586,14 +586,14 @@ describe('TopoloClient.listServices', () => {
586
586
  data: {
587
587
  services: [
588
588
  {
589
- service_id: 'srv_mail',
589
+ app_id: 'app_mail',
590
590
  app_slug: 'topolo-mail',
591
591
  name: 'Topolo Mail',
592
592
  api_base_url: 'https://mail-api.topolo.app',
593
593
  launch_url: 'https://mail.topolo.app',
594
594
  surface_kind: 'application',
595
595
  status: 'active',
596
- granted_permissions: ['srv_mail.messages:read'],
596
+ granted_permissions: ['app_mail.messages:read'],
597
597
  },
598
598
  ],
599
599
  },
@@ -621,14 +621,14 @@ describe('TopoloClient.listServices', () => {
621
621
  data: {
622
622
  services: [
623
623
  {
624
- service_id: 'srv_crm',
624
+ app_id: 'app_crm',
625
625
  app_slug: 'topolo-crm',
626
626
  name: 'Topolo CRM',
627
627
  api_base_url: 'https://crm-api.topolo.app',
628
628
  launch_url: 'https://crm.topolo.app',
629
629
  surface_kind: 'application',
630
630
  status: 'active',
631
- granted_permissions: ['srv_crm.contacts:read'],
631
+ granted_permissions: ['app_crm.contacts:read'],
632
632
  },
633
633
  ],
634
634
  },
@@ -671,14 +671,14 @@ describe('TopoloClient.listServices', () => {
671
671
  data: {
672
672
  services: [
673
673
  {
674
- service_id: 'srv_future',
674
+ app_id: 'app_future',
675
675
  app_slug: 'topolo-future',
676
676
  name: 'Future',
677
677
  api_base_url: 'https://future-api.topolo.app',
678
678
  launch_url: 'https://future.topolo.app',
679
679
  surface_kind: 'api',
680
680
  status: 'active',
681
- granted_permissions: ['srv_future.read:all'],
681
+ granted_permissions: ['app_future.read:all'],
682
682
  },
683
683
  ],
684
684
  },
@@ -705,14 +705,14 @@ describe('TopoloClient.listServices', () => {
705
705
  data: {
706
706
  services: [
707
707
  {
708
- service_id: 'srv_crm',
708
+ app_id: 'app_crm',
709
709
  app_slug: 'topolo-crm',
710
710
  name: 'Topolo CRM',
711
711
  api_base_url: 'https://crm-api.topolo.app',
712
712
  launch_url: 'https://crm.topolo.app',
713
713
  surface_kind: 'api',
714
714
  status: 'active',
715
- granted_permissions: ['srv_crm.contacts:read'],
715
+ granted_permissions: ['app_crm.contacts:read'],
716
716
  },
717
717
  ],
718
718
  },
@@ -739,13 +739,13 @@ describe('TopoloClient.listServices', () => {
739
739
  data: {
740
740
  services: [
741
741
  {
742
- service_id: 'srv_future_app',
742
+ app_id: 'app_future_app',
743
743
  app_slug: 'topolo-future-app',
744
744
  name: 'Future App',
745
745
  launch_url: 'https://future-app.topolo.app',
746
746
  surface_kind: 'application',
747
747
  status: 'active',
748
- granted_permissions: ['srv_future_app.launch:read'],
748
+ granted_permissions: ['app_future_app.launch:read'],
749
749
  launchable: true,
750
750
  },
751
751
  ],
@@ -772,14 +772,14 @@ describe('TopoloClient.listServices', () => {
772
772
  data: {
773
773
  services: [
774
774
  {
775
- service_id: 'srv_future',
775
+ app_id: 'app_future',
776
776
  app_slug: 'topolo-warehouse',
777
777
  name: 'Warehouse',
778
778
  api_base_url: 'https://warehouse.topolo.app',
779
779
  launch_url: 'https://warehouse.topolo.app',
780
780
  surface_kind: 'api',
781
781
  status: 'active',
782
- granted_permissions: ['srv_future.read:all'],
782
+ granted_permissions: ['app_future.read:all'],
783
783
  },
784
784
  ],
785
785
  },
@@ -838,7 +838,7 @@ describe('TopoloClient action catalog', () => {
838
838
  expect(url.toString()).toBe('https://mail-api.topolo.app/api/mail/mailboxes/primary/messages?q=Ada&page=2');
839
839
  expect(call.init.method).toBe('GET');
840
840
  const headers = new Headers(call.init.headers);
841
- expect(headers.get('X-Topolo-Action-Id')).toBe('srv_mail.messages.list');
841
+ expect(headers.get('X-Topolo-Action-Id')).toBe('app_mail.messages.list');
842
842
  expect(headers.get('X-Topolo-Action-Name')).toBe('messages.list');
843
843
  });
844
844
 
package/src/client.ts CHANGED
@@ -19,8 +19,8 @@ export interface TopoloClientOptions {
19
19
  agent: AgentIdentity;
20
20
  /** Per-service URL overrides. Useful for staging/dev. */
21
21
  serviceUrls?: Record<string, string>;
22
- /** Per-service TopoloAuth service id overrides from the owning app/deployment. */
23
- serviceIds?: Record<string, string>;
22
+ /** Per-service TopoloAuth app id overrides from the owning app/deployment. */
23
+ appIds?: Record<string, string>;
24
24
  /**
25
25
  * When true, mutating helpers (POST/PUT/PATCH/DELETE) require callers to pass
26
26
  * `{ confirm: true }`. Defaults to true. Set false only for trusted surfaces.
@@ -98,7 +98,7 @@ const POSITIVE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
98
98
  const NEGATIVE_CACHE_TTL_MS = 60 * 1000; // 1 minute
99
99
  const CATALOG_CACHE_TTL_MS = 60 * 1000; // 1 minute
100
100
 
101
- interface ServiceIdCacheEntry {
101
+ interface AppIdCacheEntry {
102
102
  value: string | null;
103
103
  expiresAt: number;
104
104
  }
@@ -109,7 +109,7 @@ interface ServiceIdCacheEntry {
109
109
  * from TopoloAuth `GET /api/services/catalog`.
110
110
  */
111
111
  export interface TopoloServiceCatalogEntry {
112
- serviceId: string;
112
+ appId: string;
113
113
  slug: string | null;
114
114
  name: string | null;
115
115
  description: string | null;
@@ -139,7 +139,7 @@ export interface TopoloActionCatalogEntry {
139
139
  toolName: string | null;
140
140
  title: string;
141
141
  description: string;
142
- serviceId: string;
142
+ appId: string;
143
143
  serviceSlug: string | null;
144
144
  serviceName: string | null;
145
145
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
@@ -151,6 +151,8 @@ export interface TopoloActionCatalogEntry {
151
151
  readOnly: boolean;
152
152
  destructive: boolean;
153
153
  requiresConfirmation: boolean;
154
+ /** Agent access tier. null/absent when the catalog does not specify one (consumers derive a default). */
155
+ agentAccess?: 'auto' | 'confirm' | 'off' | null;
154
156
  status: string | null;
155
157
  sourceRevision: string | null;
156
158
  }
@@ -159,7 +161,7 @@ export class TopoloClient {
159
161
  private readonly credential: TopoloCredential;
160
162
  private readonly agent: AgentIdentity;
161
163
  private readonly serviceUrls?: Record<string, string>;
162
- private readonly serviceIds?: Record<string, string>;
164
+ private readonly appIds?: Record<string, string>;
163
165
  private readonly requireConfirmForWrites: boolean;
164
166
  private readonly timeoutMs: number;
165
167
  private readonly fetchImpl: typeof fetch;
@@ -168,19 +170,19 @@ export class TopoloClient {
168
170
  private actionCatalogCache: { entries: TopoloActionCatalogEntry[]; expiresAt: number } | null = null;
169
171
 
170
172
  /**
171
- * Process-wide cache for slug → service-id lookups. Keyed by
173
+ * Process-wide cache for slug → app-id lookups. Keyed by
172
174
  * `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
173
175
  * the same Auth share, but staging vs prod don't poison each other.
174
- * Cleared via `TopoloClient.clearServiceIdCache()` if a test needs it.
176
+ * Cleared via `TopoloClient.clearAppIdCache()` if a test needs it.
175
177
  */
176
- private static readonly serviceIdCache = new Map<string, ServiceIdCacheEntry>();
178
+ private static readonly appIdCache = new Map<string, AppIdCacheEntry>();
177
179
 
178
180
  /**
179
- * Drop the slug→service-id cache. Test-only — production callers should
181
+ * Drop the slug→app-id cache. Test-only — production callers should
180
182
  * let the 5-minute TTL expire naturally.
181
183
  */
182
- static clearServiceIdCache(): void {
183
- TopoloClient.serviceIdCache.clear();
184
+ static clearAppIdCache(): void {
185
+ TopoloClient.appIdCache.clear();
184
186
  }
185
187
 
186
188
  constructor(options: TopoloClientOptions) {
@@ -189,7 +191,7 @@ export class TopoloClient {
189
191
  this.credential = options.credential;
190
192
  this.agent = options.agent;
191
193
  this.serviceUrls = options.serviceUrls;
192
- this.serviceIds = options.serviceIds;
194
+ this.appIds = options.appIds;
193
195
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
194
196
  this.timeoutMs = options.timeoutMs ?? 30_000;
195
197
  this.fetchImpl = options.fetch ?? fetch;
@@ -231,8 +233,8 @@ export class TopoloClient {
231
233
  const headers = new Headers();
232
234
  headers.set('Accept', 'application/json');
233
235
  if (opts.body !== undefined) headers.set('Content-Type', 'application/json');
234
- const platformServiceId = resolveConfiguredServiceId(resolvedService.serviceKey, this.serviceIds);
235
- if (platformServiceId) headers.set('X-Service-ID', platformServiceId);
236
+ const platformAppId = resolveConfiguredAppId(resolvedService.serviceKey, this.appIds);
237
+ if (platformAppId) headers.set('X-App-ID', platformAppId);
236
238
 
237
239
  applyAuthHeaders(headers, this.credential);
238
240
  const requestId = generateRequestId();
@@ -377,7 +379,7 @@ export class TopoloClient {
377
379
  }
378
380
 
379
381
  /**
380
- * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
382
+ * Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
381
383
  *
382
384
  * Cache: 5 minutes per process per `(slug, auth_base_url)`. Negative
383
385
  * results (404) are cached for 60s so a typo doesn't hot-loop the
@@ -388,19 +390,19 @@ export class TopoloClient {
388
390
  * This keeps the resolver usable during early bootstrap before a caller
389
391
  * has acquired an access token.
390
392
  */
391
- async getServiceIdBySlug(slug: string): Promise<string | null> {
393
+ async getAppIdBySlug(slug: string): Promise<string | null> {
392
394
  if (typeof slug !== 'string') return null;
393
395
  const normalized = slug.trim().toLowerCase();
394
396
  if (!normalized) return null;
395
397
 
396
- const configuredServiceId = resolveConfiguredServiceId(normalized, this.serviceIds);
397
- if (configuredServiceId) return configuredServiceId;
398
+ const configuredAppId = resolveConfiguredAppId(normalized, this.appIds);
399
+ if (configuredAppId) return configuredAppId;
398
400
 
399
401
  // app-catalog greenfield: by-slug resolver lives on the Catalog (developers).
400
402
  const authBase = resolveServiceUrl('developers', this.serviceUrls);
401
403
  const cacheKey = `${authBase}|${normalized}`;
402
404
  const now = Date.now();
403
- const cached = TopoloClient.serviceIdCache.get(cacheKey);
405
+ const cached = TopoloClient.appIdCache.get(cacheKey);
404
406
  if (cached && cached.expiresAt > now) {
405
407
  return cached.value;
406
408
  }
@@ -422,7 +424,7 @@ export class TopoloClient {
422
424
  }
423
425
 
424
426
  if (res.status === 404) {
425
- TopoloClient.serviceIdCache.set(cacheKey, {
427
+ TopoloClient.appIdCache.set(cacheKey, {
426
428
  value: null,
427
429
  expiresAt: now + NEGATIVE_CACHE_TTL_MS,
428
430
  });
@@ -434,11 +436,11 @@ export class TopoloClient {
434
436
  }
435
437
 
436
438
  const body = (await res.json().catch(() => null)) as
437
- | { success?: boolean; data?: { service_id?: unknown } }
439
+ | { success?: boolean; data?: { app_id?: unknown } }
438
440
  | null;
439
- const serviceId = body?.data?.service_id;
440
- const value = typeof serviceId === 'string' && serviceId ? serviceId : null;
441
- TopoloClient.serviceIdCache.set(cacheKey, {
441
+ const appId = body?.data?.app_id;
442
+ const value = typeof appId === 'string' && appId ? appId : null;
443
+ TopoloClient.appIdCache.set(cacheKey, {
442
444
  value,
443
445
  expiresAt: now + POSITIVE_CACHE_TTL_MS,
444
446
  });
@@ -481,13 +483,13 @@ export class TopoloClient {
481
483
  const entries: TopoloServiceCatalogEntry[] = [];
482
484
  for (const item of raw) {
483
485
  const s = item as Record<string, unknown>;
484
- const serviceId =
485
- typeof s.service_id === 'string'
486
- ? s.service_id
486
+ const appId =
487
+ typeof s.app_id === 'string'
488
+ ? s.app_id
487
489
  : typeof s.id === 'string'
488
490
  ? s.id
489
491
  : null;
490
- if (!serviceId) continue;
492
+ if (!appId) continue;
491
493
  const permissions = Array.isArray(s.granted_permissions)
492
494
  ? s.granted_permissions
493
495
  .filter((permission): permission is string => typeof permission === 'string' && permission.trim().length > 0)
@@ -505,7 +507,7 @@ export class TopoloClient {
505
507
  ? [apiKeyResource]
506
508
  : [];
507
509
  entries.push({
508
- serviceId,
510
+ appId,
509
511
  slug: typeof s.app_slug === 'string' ? s.app_slug : null,
510
512
  name: typeof s.name === 'string' ? s.name : null,
511
513
  description:
@@ -539,8 +541,9 @@ export class TopoloClient {
539
541
  }
540
542
 
541
543
  /**
542
- * List callable actions from the live Auth catalog. The response is scoped to
543
- * the caller's organization and effective permissions; no bundled/static app
544
+ * List callable actions from the live registry-canonical catalog (definitions
545
+ * from the Catalog registry, scoping from Auth). The response is scoped to the
546
+ * caller's organization and effective permissions; no bundled/static app
544
547
  * action list is used as an authorization source.
545
548
  */
546
549
  async listActions(options: { service?: string } = {}): Promise<TopoloActionCatalogEntry[]> {
@@ -553,7 +556,7 @@ export class TopoloClient {
553
556
  | { data?: { actions?: unknown[] } }
554
557
  | { actions?: unknown[] }
555
558
  >({
556
- service: 'auth',
559
+ service: 'developers',
557
560
  method: 'GET',
558
561
  path: '/api/actions/catalog',
559
562
  ...(options.service ? { query: { service: options.service } } : {}),
@@ -600,7 +603,7 @@ export class TopoloClient {
600
603
  ): Promise<T> {
601
604
  const entry = await this.getAction(action);
602
605
  const mapped = mapActionInput(entry, input);
603
- const service = entry.serviceSlug || entry.serviceId;
606
+ const service = entry.serviceSlug || entry.appId;
604
607
  return this.request<T>({
605
608
  service,
606
609
  method: entry.method,
@@ -626,7 +629,7 @@ export class TopoloClient {
626
629
  productionUrl?: string | null;
627
630
  }>;
628
631
  return apps.map((app) => ({
629
- serviceId: app.id,
632
+ appId: app.id,
630
633
  slug: app.id,
631
634
  name: app.name ?? app.id,
632
635
  description: null,
@@ -685,15 +688,15 @@ export class TopoloClient {
685
688
  }
686
689
  }
687
690
 
688
- function resolveConfiguredServiceId(
691
+ function resolveConfiguredAppId(
689
692
  slug: string,
690
- serviceIds?: Record<string, string>,
693
+ appIds?: Record<string, string>,
691
694
  ): string | null {
692
695
  for (const serviceKey of serviceKeyCandidates(slug)) {
693
- const configured = serviceIds?.[serviceKey as ServiceId];
696
+ const configured = appIds?.[serviceKey as ServiceId];
694
697
  if (typeof configured === 'string' && configured.trim()) return configured.trim();
695
698
 
696
- const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, '_').toUpperCase()}`;
699
+ const envKey = `TOPOLO_APP_ID_${serviceKey.replace(/\./g, '_').toUpperCase()}`;
697
700
  const envValue = typeof process !== 'undefined' ? process.env?.[envKey] : undefined;
698
701
  if (typeof envValue === 'string' && envValue.trim()) return envValue.trim();
699
702
  }
@@ -811,7 +814,7 @@ function catalogEntryMatches(entry: TopoloServiceCatalogEntry, needle: string):
811
814
  }
812
815
 
813
816
  function catalogEntryLookupKeys(entry: TopoloServiceCatalogEntry): string[] {
814
- const values = [entry.serviceId, entry.slug, entry.name].filter((value): value is string => (
817
+ const values = [entry.appId, entry.slug, entry.name].filter((value): value is string => (
815
818
  typeof value === 'string' && value.trim().length > 0
816
819
  ));
817
820
  const keys = new Set<string>();
@@ -830,10 +833,10 @@ function normalizeActionCatalogEntry(item: unknown): TopoloActionCatalogEntry |
830
833
  const a = item as Record<string, unknown>;
831
834
  const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
832
835
  const name = pickActionString(a.name);
833
- const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
836
+ const appId = pickActionString(a.app_id) ?? pickActionString(a.appId);
834
837
  const method = pickActionString(a.method)?.toUpperCase();
835
838
  const path = pickActionString(a.path);
836
- if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
839
+ if (!actionId || !name || !appId || !method || !path || !isHttpMethod(method)) {
837
840
  return null;
838
841
  }
839
842
 
@@ -843,7 +846,7 @@ function normalizeActionCatalogEntry(item: unknown): TopoloActionCatalogEntry |
843
846
  toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
844
847
  title: pickActionString(a.title) ?? name,
845
848
  description: pickActionString(a.description) ?? '',
846
- serviceId,
849
+ appId,
847
850
  serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
848
851
  serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
849
852
  method,
@@ -855,6 +858,7 @@ function normalizeActionCatalogEntry(item: unknown): TopoloActionCatalogEntry |
855
858
  readOnly: Boolean(a.read_only ?? a.readOnly),
856
859
  destructive: Boolean(a.destructive),
857
860
  requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
861
+ agentAccess: pickAgentAccess(a.agent_access ?? a.agentAccess),
858
862
  status: pickActionString(a.status),
859
863
  sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision),
860
864
  };
@@ -868,6 +872,10 @@ function pickActionString(value: unknown): string | null {
868
872
  return typeof value === 'string' && value.trim() ? value.trim() : null;
869
873
  }
870
874
 
875
+ function pickAgentAccess(value: unknown): 'auto' | 'confirm' | 'off' | null {
876
+ return value === 'auto' || value === 'confirm' || value === 'off' ? value : null;
877
+ }
878
+
871
879
  function plainObject(value: unknown): Record<string, unknown> {
872
880
  return value && typeof value === 'object' && !Array.isArray(value)
873
881
  ? value as Record<string, unknown>