@the-open-engine/zeroshot 6.32.0 → 6.33.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.
Files changed (51) hide show
  1. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  2. package/lib/agent-cli-provider/adapters/codex.js +14 -6
  3. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  4. package/lib/agent-cli-provider/provider-registry.d.ts +4 -2
  5. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  6. package/lib/agent-cli-provider/provider-registry.js +13 -3
  7. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  8. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/single-agent-runtime.js +7 -4
  10. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  11. package/lib/agent-cli-provider/types.d.ts +2 -0
  12. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  13. package/lib/agent-cli-provider/types.js.map +1 -1
  14. package/lib/detached-startup.js +4 -0
  15. package/lib/hosted-target/adapter-request.cjs +6 -3
  16. package/lib/hosted-target/adapter-request.mjs +6 -3
  17. package/lib/target/discovery-sections.cjs +35 -13
  18. package/lib/target/discovery-sections.d.cts +5 -1
  19. package/lib/target/discovery-sections.d.mts +5 -1
  20. package/lib/target/discovery-sections.d.ts +5 -1
  21. package/lib/target/discovery-sections.js +35 -13
  22. package/lib/target/discovery-sections.mjs +35 -13
  23. package/lib/target/discovery.cjs +49 -38
  24. package/lib/target/discovery.d.cts +3 -0
  25. package/lib/target/discovery.d.mts +3 -0
  26. package/lib/target/discovery.d.ts +3 -0
  27. package/lib/target/discovery.js +49 -38
  28. package/lib/target/discovery.mjs +49 -38
  29. package/lib/target/index.d.ts +1 -1
  30. package/lib/target/run-intent-discovery.cjs +30 -0
  31. package/lib/target/run-intent-discovery.d.cts +11 -0
  32. package/lib/target/run-intent-discovery.d.mts +11 -0
  33. package/lib/target/run-intent-discovery.d.ts +11 -0
  34. package/lib/target/run-intent-discovery.js +30 -0
  35. package/lib/target/run-intent-discovery.mjs +27 -0
  36. package/npm-shrinkwrap.json +2 -2
  37. package/package.json +2 -3
  38. package/src/agent/agent-lifecycle.js +11 -1
  39. package/src/agent/agent-task-executor.js +25 -7
  40. package/src/agent/structured-output-error.js +42 -0
  41. package/src/agent-cli-provider/adapters/codex.ts +15 -16
  42. package/src/agent-cli-provider/provider-registry.ts +15 -3
  43. package/src/agent-cli-provider/single-agent-runtime.ts +11 -11
  44. package/src/agent-cli-provider/types.ts +2 -0
  45. package/src/hosted-target/adapter-request.ts +8 -3
  46. package/src/isolation-manager.js +52 -0
  47. package/src/orchestrator.js +6 -0
  48. package/src/target/discovery-sections.ts +59 -41
  49. package/src/target/discovery.ts +71 -54
  50. package/src/target/index.ts +1 -0
  51. package/src/target/run-intent-discovery.ts +39 -0
@@ -276,9 +276,9 @@ export const providerRegistry = [
276
276
  authInstructions: 'codex login',
277
277
  credentialPaths: ['~/.config/codex', '~/.codex'],
278
278
  credentialEnvKeys: codexAdapter.credentialEnvKeys,
279
- settingsFields: ['webSearch'],
280
- settingsDefaults: { webSearch: false },
281
- settingsValidator: (settings): string | null => validateWebSearchSettings('codex', settings),
279
+ settingsFields: ['webSearch', 'trustIsolatedRecoveryProfile'],
280
+ settingsDefaults: { webSearch: false, trustIsolatedRecoveryProfile: false },
281
+ settingsValidator: validateCodexSettings,
282
282
  capabilities: {
283
283
  ...STANDARD_CAPABILITIES,
284
284
  jsonSchema: true,
@@ -662,6 +662,18 @@ export const providerRegistry = [
662
662
  },
663
663
  ] as const satisfies readonly ProviderRegistryEntry[];
664
664
 
665
+ function validateCodexSettings(settings: Record<string, unknown>): string | null {
666
+ const webSearchError = validateWebSearchSettings('codex', settings);
667
+ if (webSearchError) return webSearchError;
668
+ if (
669
+ settings.trustIsolatedRecoveryProfile === undefined ||
670
+ typeof settings.trustIsolatedRecoveryProfile === 'boolean'
671
+ ) {
672
+ return null;
673
+ }
674
+ return 'providerSettings.codex.trustIsolatedRecoveryProfile must be a boolean';
675
+ }
676
+
665
677
  function validateWebSearchSettings(
666
678
  provider: 'codex' | 'opencode',
667
679
  settings: Record<string, unknown>
@@ -54,16 +54,12 @@ import type {
54
54
 
55
55
  type UnknownFunction = (...args: readonly unknown[]) => unknown;
56
56
 
57
- interface CommandParts {
58
- readonly command: string;
59
- readonly args: readonly string[];
60
- }
61
-
62
57
  interface RuntimeProviderSettings {
63
58
  readonly defaultLevel?: ModelLevel;
64
59
  readonly levelOverrides: LevelOverrides;
65
60
  readonly gateway?: GatewayBuildOptions;
66
61
  readonly webSearch?: boolean;
62
+ readonly trustIsolatedRecoveryProfile?: boolean;
67
63
  }
68
64
 
69
65
  interface RuntimeCommandContext {
@@ -428,7 +424,6 @@ function ompExecutionContext(
428
424
  throw new Error('options.executionContext must be "host", "detached", "docker", or "benchmark".');
429
425
  }
430
426
 
431
-
432
427
  function ompSdkOutputContract(
433
428
  options: BuildProviderCommandOptions
434
429
  ):
@@ -664,7 +659,7 @@ export function probeRuntimeProviderCli(
664
659
  const requested = registryEntry.settingsFields.includes('webSearch')
665
660
  ? runtimeProviderSettings(settings, adapter.id, process.cwd()).webSearch === true
666
661
  : false;
667
- const helpCommand = runtimeHelpCommand(adapter.id);
662
+ const helpCommand = resolveProviderCommand(adapter.id);
668
663
  const commandAvailable =
669
664
  evidence === undefined
670
665
  ? booleanResult(commandExistsFn(helpCommand.command))
@@ -735,6 +730,10 @@ function buildRuntimeOptions(
735
730
  cliFeatures: runtime.cliFeatures,
736
731
  };
737
732
  const resolved = { ...baseResolved };
733
+ delete resolved.trustIsolatedCodexProfile;
734
+ if (baseOptions.structuredOutputRecovery && adapter.id === 'codex') {
735
+ resolved.trustIsolatedCodexProfile = providerSettings.trustIsolatedRecoveryProfile === true;
736
+ }
738
737
  if (baseOptions.structuredOutputRecovery) {
739
738
  delete resolved.resumeSessionId;
740
739
  delete resolved.continueSession;
@@ -870,6 +869,10 @@ function runtimeProviderSettings(
870
869
  providerSettings.webSearch,
871
870
  `settings.providerSettings.${provider}.webSearch`
872
871
  );
872
+ const trustIsolatedRecoveryProfile = optionalBoolean(
873
+ providerSettings.trustIsolatedRecoveryProfile,
874
+ `settings.providerSettings.${provider}.trustIsolatedRecoveryProfile`
875
+ );
873
876
  const gateway =
874
877
  provider === 'gateway'
875
878
  ? normalizeGatewayBuildOptions(providerSettings, 'settings.providerSettings.gateway', cwd)
@@ -878,14 +881,11 @@ function runtimeProviderSettings(
878
881
  levelOverrides,
879
882
  ...(gateway === undefined ? {} : { gateway }),
880
883
  ...(webSearch === undefined ? {} : { webSearch }),
884
+ ...(trustIsolatedRecoveryProfile === undefined ? {} : { trustIsolatedRecoveryProfile }),
881
885
  };
882
886
  return defaultLevel === undefined ? base : { ...base, defaultLevel };
883
887
  }
884
888
 
885
- function runtimeHelpCommand(provider: ProviderId): CommandParts {
886
- return resolveProviderCommand(provider);
887
- }
888
-
889
889
  function probeGatewayProvider(
890
890
  adapter: ProviderAdapter,
891
891
  runtimeSettings?: Record<string, unknown>
@@ -468,6 +468,8 @@ export interface BuildProviderCommandOptions {
468
468
  readonly mcpConfig?: readonly string[];
469
469
  /** Internal profile for provider-neutral structured-output correction turns. */
470
470
  readonly structuredOutputRecovery?: boolean;
471
+ /** Preserve a caller-isolated CODEX_HOME profile during Codex recovery. */
472
+ readonly trustIsolatedCodexProfile?: boolean;
471
473
  }
472
474
 
473
475
  export interface TextEvent {
@@ -19,9 +19,14 @@ export type ExecuteArguments<T> = [
19
19
  ];
20
20
 
21
21
  export function requestUrl(path: string, descriptor: TargetDiscoveryDescriptor): URL {
22
- const url = new globalThis.URL(path, descriptor.capsule.baseUrl);
23
- if (url.origin !== descriptor.origin || url.hash ||
24
- `${url.pathname}${url.search}` !== path) {
22
+ const baseUrl = new globalThis.URL(descriptor.capsule.baseUrl);
23
+ const requestPath = `${baseUrl.pathname.replace(/\/$/, '')}${path}`;
24
+ const url = new globalThis.URL(requestPath, baseUrl.origin);
25
+ if (
26
+ url.origin !== descriptor.origin ||
27
+ url.hash ||
28
+ `${url.pathname}${url.search}` !== requestPath
29
+ ) {
25
30
  throw new TargetProtocolError('Capsule route changed during URL canonicalization');
26
31
  }
27
32
  return url;
@@ -36,6 +36,40 @@ const { provisionClaudeCredentials } = require('./claude-credentials');
36
36
 
37
37
  const DEFAULT_WORKTREE_SETUP_TIMEOUT_MS = 15 * 60 * 1000;
38
38
  const FRESH_BASE_REF_PREFIX = 'refs/zeroshot/base-fetch';
39
+ const DETACHED_SETUP_CLUSTER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
40
+ const DETACHED_SETUP_RESOURCE_KEYS = ['configDir', 'containerName', 'isolatedDir', 'kind'];
41
+
42
+ function getDetachedSetupResources(clusterId) {
43
+ if (typeof clusterId !== 'string' || !DETACHED_SETUP_CLUSTER_ID_PATTERN.test(clusterId)) {
44
+ throw new Error(`Invalid detached setup cluster ID: ${clusterId}`);
45
+ }
46
+
47
+ return Object.freeze({
48
+ kind: 'docker',
49
+ containerName: `zeroshot-cluster-${clusterId}`,
50
+ isolatedDir: path.join(os.tmpdir(), 'zeroshot-isolated', clusterId),
51
+ configDir: path.join(os.tmpdir(), 'zeroshot-cluster-configs', clusterId),
52
+ });
53
+ }
54
+
55
+ function setupResourcesMatch(recorded, expected) {
56
+ if (!recorded || typeof recorded !== 'object' || Array.isArray(recorded)) return false;
57
+ const keys = Object.keys(recorded).sort();
58
+ return (
59
+ keys.length === DETACHED_SETUP_RESOURCE_KEYS.length &&
60
+ keys.every((key, index) => key === DETACHED_SETUP_RESOURCE_KEYS[index]) &&
61
+ keys.every((key) => recorded[key] === expected[key])
62
+ );
63
+ }
64
+
65
+ function removeBoundedSetupDirectory(root, target) {
66
+ const resolvedRoot = path.resolve(root);
67
+ const resolvedTarget = path.resolve(target);
68
+ if (path.dirname(resolvedTarget) !== resolvedRoot) {
69
+ throw new Error(`Refusing to remove setup directory outside ${resolvedRoot}`);
70
+ }
71
+ fs.rmSync(resolvedTarget, { recursive: true, force: true });
72
+ }
39
73
 
40
74
  function runSync(command, args, options = {}) {
41
75
  const timeout = options.timeout ?? 30000;
@@ -364,6 +398,24 @@ class IsolationManager {
364
398
  return this._isContainerRunning(existingId) ? existingId : null;
365
399
  }
366
400
 
401
+ static getDetachedSetupResources(clusterId) {
402
+ return getDetachedSetupResources(clusterId);
403
+ }
404
+
405
+ cleanupDetachedSetupResources(clusterId, recordedResources) {
406
+ const expected = getDetachedSetupResources(clusterId);
407
+ if (!setupResourcesMatch(recordedResources, expected)) {
408
+ throw new Error(`Detached setup resources do not match cluster ${clusterId}`);
409
+ }
410
+
411
+ this._removeContainerByName(expected.containerName);
412
+ removeBoundedSetupDirectory(path.join(os.tmpdir(), 'zeroshot-isolated'), expected.isolatedDir);
413
+ removeBoundedSetupDirectory(
414
+ path.join(os.tmpdir(), 'zeroshot-cluster-configs'),
415
+ expected.configDir
416
+ );
417
+ }
418
+
367
419
  async _prepareIsolatedWorkspace(clusterId, workDir, reuseExisting) {
368
420
  if (!this._isGitRepo(workDir)) {
369
421
  return workDir;
@@ -2221,6 +2221,12 @@ class Orchestrator {
2221
2221
  async _killSetupCluster(clusterId, cluster) {
2222
2222
  await this._signalSetupProcess(clusterId, cluster);
2223
2223
 
2224
+ if (cluster.setupResources) {
2225
+ this._log(`[Orchestrator] Cleaning provisional setup resources for ${clusterId}...`);
2226
+ const manager = new IsolationManager();
2227
+ manager.cleanupDetachedSetupResources(clusterId, cluster.setupResources);
2228
+ }
2229
+
2224
2230
  if (cluster.worktree?.path) {
2225
2231
  this._teardownWorktreeCompose(cluster.worktree.path);
2226
2232
  try {
@@ -9,6 +9,7 @@ import {
9
9
  stringField,
10
10
  type CredentialInstallDescriptor,
11
11
  } from './discovery-validation.js';
12
+ import { parseRunIntent, type RunIntentDescriptor } from './run-intent-discovery.js';
12
13
  import { routeTemplate } from './route-template.js';
13
14
 
14
15
  export function parseAdapter(discovery: Record<string, unknown>): {
@@ -29,8 +30,11 @@ export function parseAdapter(discovery: Record<string, unknown>): {
29
30
  export function validateCachePolicy(discovery: Record<string, unknown>): void {
30
31
  const cache = closedRecord(discovery.cache_policy, 'cache_policy', ['control', 'discovery']);
31
32
  exact(cache.control, 'no-store', 'cache_policy.control');
32
- if (cache.discovery !== undefined && cache.discovery !== null &&
33
- typeof cache.discovery !== 'string') {
33
+ if (
34
+ cache.discovery !== undefined &&
35
+ cache.discovery !== null &&
36
+ typeof cache.discovery !== 'string'
37
+ ) {
34
38
  throw new TargetDiscoveryError('cache_policy.discovery must be a string or null');
35
39
  }
36
40
  }
@@ -40,9 +44,12 @@ export function parseSizes(discovery: Record<string, unknown>): {
40
44
  readonly default: 'tiny' | 'small' | 'standard' | 'large';
41
45
  } {
42
46
  const sizes = closedRecord(discovery.sizes, 'sizes', ['catalog', 'default']);
43
- if (!Array.isArray(sizes.catalog) || sizes.catalog.length === 0 ||
44
- new Set(sizes.catalog).size !== sizes.catalog.length ||
45
- sizes.catalog.some((size) => !['tiny', 'small', 'standard', 'large'].includes(String(size)))) {
47
+ if (
48
+ !Array.isArray(sizes.catalog) ||
49
+ sizes.catalog.length === 0 ||
50
+ new Set(sizes.catalog).size !== sizes.catalog.length ||
51
+ sizes.catalog.some((size) => !['tiny', 'small', 'standard', 'large'].includes(String(size)))
52
+ ) {
46
53
  throw new TargetDiscoveryError('sizes.catalog contains an unsupported size');
47
54
  }
48
55
  if (!sizes.catalog.includes(sizes.default)) {
@@ -56,44 +63,58 @@ export function parseSizes(discovery: Record<string, unknown>): {
56
63
 
57
64
  export function parseExtensions(
58
65
  discovery: Record<string, unknown>,
59
- origin: string,
60
- ): CredentialInstallDescriptor | null {
61
- if (discovery.extensions === undefined || discovery.extensions === null) return null;
66
+ origin: string
67
+ ): {
68
+ readonly credentialInstall: CredentialInstallDescriptor | null;
69
+ readonly runIntent: RunIntentDescriptor | null;
70
+ } {
71
+ if (discovery.extensions === undefined || discovery.extensions === null) {
72
+ return Object.freeze({ credentialInstall: null, runIntent: null });
73
+ }
62
74
  const extensions = closedRecord(discovery.extensions, 'extensions', [
63
- 'connections', 'credential_install',
75
+ 'connections',
76
+ 'credential_install',
77
+ 'run_intent',
64
78
  ]);
65
79
  if (extensions.connections === undefined) {
66
80
  throw new TargetDiscoveryError('extensions.connections is required');
67
81
  }
68
82
  const connections = closedRecord(extensions.connections, 'extensions.connections', [
69
- 'kind', 'base_url', 'route_templates',
83
+ 'kind',
84
+ 'base_url',
85
+ 'route_templates',
70
86
  ]);
71
87
  exact(connections.kind, 'zerocloud.connections/v1', 'extensions.connections.kind');
72
88
  sameOriginUrl(connections.base_url, 'extensions.connections.base_url', origin);
73
- const routes = closedRecord(connections.route_templates, 'extensions.connections.route_templates', [
74
- 'list', 'create', 'update',
75
- ]);
89
+ const routes = closedRecord(
90
+ connections.route_templates,
91
+ 'extensions.connections.route_templates',
92
+ ['list', 'create', 'update']
93
+ );
76
94
  routeTemplate(routes.list, 'extensions.connections.route_templates.list', []);
77
95
  routeTemplate(routes.create, 'extensions.connections.route_templates.create', []);
78
96
  routeTemplate(routes.update, 'extensions.connections.route_templates.update', ['connection_id']);
79
- return parseCredentialInstall(extensions.credential_install);
97
+ return Object.freeze({
98
+ credentialInstall: parseCredentialInstall(extensions.credential_install),
99
+ runIntent: parseRunIntent(extensions.run_intent, origin),
100
+ });
80
101
  }
81
102
 
82
103
  export function validateOAuthMetadata(
83
104
  metadata: Record<string, unknown>,
84
105
  origin: string,
85
- expected: readonly [string, string, string],
106
+ expected: readonly [string, string, string]
86
107
  ): void {
87
108
  const device = sameOriginUrl(
88
109
  metadata.device_authorization_endpoint,
89
110
  'OAuth metadata device_authorization_endpoint',
90
- origin,
111
+ origin
91
112
  );
92
113
  const token = sameOriginUrl(metadata.token_endpoint, 'OAuth metadata token_endpoint', origin);
93
114
  const revoke = sameOriginUrl(
94
115
  metadata.revocation_endpoint,
95
116
  'OAuth metadata revocation_endpoint',
96
- origin,
117
+ origin
97
118
  );
98
119
  if (device !== expected[0] || token !== expected[1] || revoke !== expected[2]) {
99
120
  throw new TargetDiscoveryError('OAuth metadata does not match hosted-target discovery');
@@ -108,20 +129,15 @@ export function parseEndpoint(discovery: Record<string, unknown>, origin: string
108
129
  exact(binding.scope, 'organization', 'binding.scope');
109
130
  exact(binding.auth_location, 'authorization_header', 'binding.auth_location');
110
131
  const endpoint = closedRecord(discovery.endpoint, 'endpoint', ['url', 'capabilities']);
111
- const capabilities = exactStringSet(
112
- endpoint.capabilities,
113
- 'endpoint.capabilities',
114
- ['exec', 'log_stream'],
115
- ) as readonly ['exec', 'log_stream'];
132
+ const capabilities = exactStringSet(endpoint.capabilities, 'endpoint.capabilities', [
133
+ 'exec',
134
+ 'log_stream',
135
+ ]) as readonly ['exec', 'log_stream'];
116
136
  const pagination = closedRecord(discovery.pagination, 'pagination', [
117
137
  'default_page_size',
118
138
  'max_page_size',
119
139
  ]);
120
- const defaultPageSize = integer(
121
- pagination.default_page_size,
122
- 'pagination.default_page_size',
123
- 1,
124
- );
140
+ const defaultPageSize = integer(pagination.default_page_size, 'pagination.default_page_size', 1);
125
141
  const maxPageSize = integer(pagination.max_page_size, 'pagination.max_page_size', 1);
126
142
  if (defaultPageSize > maxPageSize) {
127
143
  throw new TargetDiscoveryError('pagination.default_page_size exceeds maximum');
@@ -151,11 +167,14 @@ export function parseCapsule(discovery: Record<string, unknown>, origin: string)
151
167
  ]);
152
168
  exact(capsule.name, 'openengine.capsules/v1', 'capsule_protocol.name');
153
169
  exact(capsule.major_version, 1, 'capsule_protocol.major_version');
154
- const routes = closedRecord(
155
- capsule.route_templates,
156
- 'capsule_protocol.route_templates',
157
- ['allocate', 'list', 'inspect', 'terminate', 'limits', 'access'],
158
- );
170
+ const routes = closedRecord(capsule.route_templates, 'capsule_protocol.route_templates', [
171
+ 'allocate',
172
+ 'list',
173
+ 'inspect',
174
+ 'terminate',
175
+ 'limits',
176
+ 'access',
177
+ ]);
159
178
  const route = (name: string, variables: readonly string[]) =>
160
179
  routeTemplate(routes[name], `capsule_protocol.route_templates.${name}`, variables);
161
180
  return Object.freeze({
@@ -185,26 +204,25 @@ export function parseOAuth(discovery: Record<string, unknown>, origin: string) {
185
204
  exact(
186
205
  oauth.device_grant_type,
187
206
  'urn:ietf:params:oauth:grant-type:device_code',
188
- 'oauth.device_grant_type',
189
- );
190
- exactStringSet(
191
- oauth.device_exchange_fields,
192
- 'oauth.device_exchange_fields',
193
- ['device_token', 'device_label'],
207
+ 'oauth.device_grant_type'
194
208
  );
209
+ exactStringSet(oauth.device_exchange_fields, 'oauth.device_exchange_fields', [
210
+ 'device_token',
211
+ 'device_label',
212
+ ]);
195
213
  exact(oauth.audience, 'capsule', 'oauth.audience');
196
214
  return Object.freeze({
197
215
  metadataUrl: sameOriginUrl(oauth.metadata_url, 'oauth.metadata_url', origin),
198
216
  deviceAuthorizationEndpoint: sameOriginUrl(
199
217
  oauth.device_authorization_endpoint,
200
218
  'oauth.device_authorization_endpoint',
201
- origin,
219
+ origin
202
220
  ),
203
221
  tokenEndpoint: sameOriginUrl(oauth.token_endpoint, 'oauth.token_endpoint', origin),
204
222
  revocationEndpoint: sameOriginUrl(
205
223
  oauth.revocation_endpoint,
206
224
  'oauth.revocation_endpoint',
207
- origin,
225
+ origin
208
226
  ),
209
227
  clientId: stringField(oauth, 'client_id', 'oauth.'),
210
228
  deviceGrantType: 'urn:ietf:params:oauth:grant-type:device_code' as const,
@@ -243,7 +261,7 @@ export function parseTransport(discovery: Record<string, unknown>) {
243
261
  websocketRouteTemplate: routeTemplate(
244
262
  transport.websocket_route_template,
245
263
  'transport.websocket_route_template',
246
- ['capsule_id'],
264
+ ['capsule_id']
247
265
  ),
248
266
  unauthorizedStatus: 401 as const,
249
267
  closeCodes: Object.freeze({ expired: 4401 as const, revoked: 4403 as const }),
@@ -8,6 +8,7 @@ import {
8
8
  record,
9
9
  type CredentialInstallDescriptor,
10
10
  } from './discovery-validation.js';
11
+ import type { RunIntentDescriptor } from './run-intent-discovery.js';
11
12
  import {
12
13
  parseAdapter,
13
14
  parseCapsule,
@@ -20,6 +21,7 @@ import {
20
21
  validateOAuthMetadata,
21
22
  } from './discovery-sections.js';
22
23
  export type { CredentialInstallDescriptor } from './discovery-validation.js';
24
+ export type { RunIntentDescriptor } from './run-intent-discovery.js';
23
25
  export { TargetDiscoveryError } from './discovery-errors.js';
24
26
  export { expandRoute, type RouteTemplate } from './route-template.js';
25
27
 
@@ -84,6 +86,7 @@ export interface TargetDiscoveryDescriptor {
84
86
  };
85
87
  readonly capabilityFlags: readonly string[];
86
88
  readonly credentialInstall: CredentialInstallDescriptor | null;
89
+ readonly runIntent: RunIntentDescriptor | null;
87
90
  readonly additional: Readonly<Record<string, unknown>>;
88
91
  }
89
92
 
@@ -100,69 +103,37 @@ export interface TargetSessionEndpoints {
100
103
  readonly descriptor: TargetDiscoveryDescriptor;
101
104
  }
102
105
 
106
+ const FLAGS = [
107
+ 'capsule_allocate',
108
+ 'capsule_read',
109
+ 'capsule_terminate',
110
+ 'capsule_access',
111
+ 'connections_onboarding',
112
+ ] as const;
113
+ const SIZE_ERROR = 'response exceeds the size limit';
114
+ const JSON_ERROR = 'response is not valid UTF-8 JSON';
103
115
 
104
-
105
- async function readBoundedJson(response: Response): Promise<unknown> {
106
- return readBoundedResponseJson(response, MAX_DISCOVERY_BYTES, (kind) =>
107
- new TargetDiscoveryError(
108
- kind === 'size' ? 'response exceeds the size limit' : 'response is not valid UTF-8 JSON',
109
- ),
110
- );
111
- }
112
-
113
-
114
- async function fetchDocument(http: HttpTransport, url: string): Promise<Record<string, unknown>> {
115
- const response = await http.fetch(url, {
116
- method: 'GET',
117
- headers: { Accept: 'application/json' },
118
- redirect: 'error',
119
- });
120
- if (response.url && new URL(response.url).href !== url) {
121
- await response.body?.cancel().catch(() => undefined);
122
- throw new TargetDiscoveryError('request changed target route or authority');
123
- }
124
- if (!response.ok) {
125
- await response.body?.cancel().catch(() => undefined);
126
- throw new TargetDiscoveryError(`request failed with status ${response.status}`);
127
- }
128
- return record(await readBoundedJson(response), 'response');
129
- }
130
-
131
-
132
- export async function discoverTarget(targetUrl: string, http: HttpTransport): Promise<TargetDiscoveryDescriptor> {
133
- const target = new URL(targetUrl);
134
- const origin = target.origin;
135
- const discovery = await fetchDocument(http, new URL(DISCOVERY_PATH, target).href);
116
+ function parseDiscoveryDocument(
117
+ discovery: Record<string, unknown>,
118
+ origin: string
119
+ ): TargetDiscoveryDescriptor {
136
120
  exact(discovery.kind, 'openengine.hosted-target/v1', 'kind');
137
-
138
121
  const adapter = parseAdapter(discovery);
139
122
  const endpoint = parseEndpoint(discovery, origin);
140
123
  const capsule = parseCapsule(discovery, origin);
141
124
  const oauth = parseOAuth(discovery, origin);
142
125
  exact(discovery.organization_binding, 'device_approval', 'organization_binding');
143
- const capabilityFlags = exactStringSet(discovery.capability_flags, 'capability_flags', [
144
- 'capsule_allocate',
145
- 'capsule_read',
146
- 'capsule_terminate',
147
- 'capsule_access',
148
- 'connections_onboarding',
149
- ]);
126
+ const capabilityFlags = exactStringSet(discovery.capability_flags, 'capability_flags', FLAGS);
150
127
  const sizes = parseSizes(discovery);
151
128
  const session = parseSession(discovery);
152
129
  const transport = parseTransport(discovery);
153
- const credentialInstall = parseExtensions(discovery, origin);
154
-
155
- const metadata = await fetchDocument(http, oauth.metadataUrl);
156
- validateOAuthMetadata(metadata, origin, [
157
- oauth.deviceAuthorizationEndpoint,
158
- oauth.tokenEndpoint,
159
- oauth.revocationEndpoint,
160
- ]);
161
-
130
+ const extensions = parseExtensions(discovery, origin);
162
131
  const additional = Object.freeze(
163
- Object.fromEntries(Object.entries(discovery).filter(([key]) =>
164
- !ROOT_FIELDS.includes(key as (typeof ROOT_FIELDS)[number]),
165
- )),
132
+ Object.fromEntries(
133
+ Object.entries(discovery).filter(
134
+ ([key]) => !ROOT_FIELDS.includes(key as (typeof ROOT_FIELDS)[number])
135
+ )
136
+ )
166
137
  );
167
138
  return Object.freeze({
168
139
  origin,
@@ -176,12 +147,33 @@ export async function discoverTarget(targetUrl: string, http: HttpTransport): Pr
176
147
  capsule,
177
148
  transport,
178
149
  capabilityFlags,
179
- credentialInstall,
150
+ credentialInstall: extensions.credentialInstall,
151
+ runIntent: extensions.runIntent,
180
152
  additional,
181
153
  });
182
154
  }
183
155
 
184
- export async function discoverTargetSessionEndpoints(targetUrl: string, http: HttpTransport): Promise<TargetSessionEndpoints> {
156
+ export async function discoverTarget(
157
+ targetUrl: string,
158
+ http: HttpTransport
159
+ ): Promise<TargetDiscoveryDescriptor> {
160
+ const target = new URL(targetUrl);
161
+ const origin = target.origin;
162
+ const discovery = await fetchDocument(http, new URL(DISCOVERY_PATH, target).href);
163
+ const descriptor = parseDiscoveryDocument(discovery, origin);
164
+ const metadata = await fetchDocument(http, descriptor.oauth.metadataUrl);
165
+ validateOAuthMetadata(metadata, origin, [
166
+ descriptor.oauth.deviceAuthorizationEndpoint,
167
+ descriptor.oauth.tokenEndpoint,
168
+ descriptor.oauth.revocationEndpoint,
169
+ ]);
170
+ return descriptor;
171
+ }
172
+
173
+ export async function discoverTargetSessionEndpoints(
174
+ targetUrl: string,
175
+ http: HttpTransport
176
+ ): Promise<TargetSessionEndpoints> {
185
177
  const descriptor = await discoverTarget(targetUrl, http);
186
178
  return Object.freeze({
187
179
  deviceAuthorizationEndpoint: descriptor.oauth.deviceAuthorizationEndpoint,
@@ -195,3 +187,28 @@ export async function discoverTargetSessionEndpoints(targetUrl: string, http: Ht
195
187
  descriptor,
196
188
  });
197
189
  }
190
+
191
+ function boundedResponseError(kind: 'size' | 'json'): Error {
192
+ return new TargetDiscoveryError(kind === 'size' ? SIZE_ERROR : JSON_ERROR);
193
+ }
194
+
195
+ async function readBoundedJson(response: Response): Promise<unknown> {
196
+ return readBoundedResponseJson(response, MAX_DISCOVERY_BYTES, boundedResponseError);
197
+ }
198
+
199
+ async function fetchDocument(http: HttpTransport, url: string): Promise<Record<string, unknown>> {
200
+ const response = await http.fetch(url, {
201
+ method: 'GET',
202
+ headers: { Accept: 'application/json' },
203
+ redirect: 'error',
204
+ });
205
+ if (response.url && new URL(response.url).href !== url) {
206
+ await response.body?.cancel().catch(() => undefined);
207
+ throw new TargetDiscoveryError('request changed target route or authority');
208
+ }
209
+ if (!response.ok) {
210
+ await response.body?.cancel().catch(() => undefined);
211
+ throw new TargetDiscoveryError(`request failed with status ${response.status}`);
212
+ }
213
+ return record(await readBoundedJson(response), 'response');
214
+ }
@@ -53,6 +53,7 @@ export {
53
53
  expandRoute,
54
54
  TargetDiscoveryError,
55
55
  type CredentialInstallDescriptor,
56
+ type RunIntentDescriptor,
56
57
  type RouteTemplate,
57
58
  type TargetDiscoveryDescriptor,
58
59
  type TargetSessionEndpoints,
@@ -0,0 +1,39 @@
1
+ import type { RouteTemplate } from './route-template.js';
2
+ import { routeTemplate } from './route-template.js';
3
+ import { closedRecord, exact, sameOriginUrl } from './discovery-validation.js';
4
+
5
+ export interface RunIntentDescriptor {
6
+ readonly kind: 'zeroshot.run-intent/v2';
7
+ readonly baseUrl: string;
8
+ readonly routes: {
9
+ readonly submit: RouteTemplate;
10
+ readonly status: RouteTemplate;
11
+ readonly cancel: RouteTemplate;
12
+ };
13
+ }
14
+
15
+ export function parseRunIntent(value: unknown, origin: string): RunIntentDescriptor | null {
16
+ if (value === undefined || value === null) return null;
17
+ const extension = closedRecord(value, 'extensions.run_intent', [
18
+ 'kind',
19
+ 'base_url',
20
+ 'route_templates',
21
+ ]);
22
+ exact(extension.kind, 'zeroshot.run-intent/v2', 'extensions.run_intent.kind');
23
+ const routes = closedRecord(extension.route_templates, 'extensions.run_intent.route_templates', [
24
+ 'submit',
25
+ 'status',
26
+ 'cancel',
27
+ ]);
28
+ const route = (name: string, variables: readonly string[]) =>
29
+ routeTemplate(routes[name], `extensions.run_intent.route_templates.${name}`, variables);
30
+ return Object.freeze({
31
+ kind: 'zeroshot.run-intent/v2' as const,
32
+ baseUrl: sameOriginUrl(extension.base_url, 'extensions.run_intent.base_url', origin),
33
+ routes: Object.freeze({
34
+ submit: route('submit', ['org_id']),
35
+ status: route('status', ['org_id', 'intent_id']),
36
+ cancel: route('cancel', ['org_id', 'intent_id']),
37
+ }),
38
+ });
39
+ }