@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.
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +14 -6
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +4 -2
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +13 -3
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +7 -4
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +2 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/detached-startup.js +4 -0
- package/lib/hosted-target/adapter-request.cjs +6 -3
- package/lib/hosted-target/adapter-request.mjs +6 -3
- package/lib/target/discovery-sections.cjs +35 -13
- package/lib/target/discovery-sections.d.cts +5 -1
- package/lib/target/discovery-sections.d.mts +5 -1
- package/lib/target/discovery-sections.d.ts +5 -1
- package/lib/target/discovery-sections.js +35 -13
- package/lib/target/discovery-sections.mjs +35 -13
- package/lib/target/discovery.cjs +49 -38
- package/lib/target/discovery.d.cts +3 -0
- package/lib/target/discovery.d.mts +3 -0
- package/lib/target/discovery.d.ts +3 -0
- package/lib/target/discovery.js +49 -38
- package/lib/target/discovery.mjs +49 -38
- package/lib/target/index.d.ts +1 -1
- package/lib/target/run-intent-discovery.cjs +30 -0
- package/lib/target/run-intent-discovery.d.cts +11 -0
- package/lib/target/run-intent-discovery.d.mts +11 -0
- package/lib/target/run-intent-discovery.d.ts +11 -0
- package/lib/target/run-intent-discovery.js +30 -0
- package/lib/target/run-intent-discovery.mjs +27 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -3
- package/src/agent/agent-lifecycle.js +11 -1
- package/src/agent/agent-task-executor.js +25 -7
- package/src/agent/structured-output-error.js +42 -0
- package/src/agent-cli-provider/adapters/codex.ts +15 -16
- package/src/agent-cli-provider/provider-registry.ts +15 -3
- package/src/agent-cli-provider/single-agent-runtime.ts +11 -11
- package/src/agent-cli-provider/types.ts +2 -0
- package/src/hosted-target/adapter-request.ts +8 -3
- package/src/isolation-manager.js +52 -0
- package/src/orchestrator.js +6 -0
- package/src/target/discovery-sections.ts +59 -41
- package/src/target/discovery.ts +71 -54
- package/src/target/index.ts +1 -0
- 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:
|
|
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 =
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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;
|
package/src/isolation-manager.js
CHANGED
|
@@ -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;
|
package/src/orchestrator.js
CHANGED
|
@@ -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 (
|
|
33
|
-
|
|
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 (
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
):
|
|
61
|
-
|
|
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',
|
|
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',
|
|
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(
|
|
74
|
-
|
|
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
|
|
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
|
-
|
|
113
|
-
'
|
|
114
|
-
|
|
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
|
-
|
|
156
|
-
'
|
|
157
|
-
|
|
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 }),
|
package/src/target/discovery.ts
CHANGED
|
@@ -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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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(
|
|
164
|
-
|
|
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
|
|
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
|
+
}
|
package/src/target/index.ts
CHANGED
|
@@ -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
|
+
}
|