@the-open-engine/zeroshot 6.27.0 → 6.29.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 (59) hide show
  1. package/README.md +3 -3
  2. package/cli/index.js +28 -21
  3. package/docker/zeroshot-cluster/Dockerfile +2 -3
  4. package/docker/zeroshot-oecp/Cargo.toml +12 -0
  5. package/docker/zeroshot-oecp/Dockerfile +65 -0
  6. package/docker/zeroshot-oecp/src/main.rs +32 -0
  7. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/adapters/codex.js +1 -0
  9. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  10. package/lib/cluster-worker/engine-adapter.js +11 -15
  11. package/lib/cluster-worker/engine-input.js +14 -0
  12. package/lib/cluster-worker/profiles.js +42 -1
  13. package/lib/start-cluster.js +3 -3
  14. package/lib/target/bounded-json.d.ts +6 -0
  15. package/lib/target/bounded-json.js +43 -0
  16. package/lib/target/credential-lock.d.ts +1 -0
  17. package/lib/target/credential-lock.js +38 -0
  18. package/lib/target/credential-store.d.ts +27 -0
  19. package/lib/target/credential-store.js +113 -0
  20. package/lib/target/device-flow.d.ts +42 -0
  21. package/lib/target/device-flow.js +109 -0
  22. package/lib/target/discovery.d.ts +12 -0
  23. package/lib/target/discovery.js +97 -0
  24. package/lib/target/hosted-run/client.d.ts +26 -0
  25. package/lib/target/hosted-run/client.js +158 -0
  26. package/lib/target/hosted-run/commands.d.ts +4 -0
  27. package/lib/target/hosted-run/commands.js +113 -0
  28. package/lib/target/hosted-run/contracts.d.ts +49 -0
  29. package/lib/target/hosted-run/contracts.js +3 -0
  30. package/lib/target/hosted-run/input.d.ts +5 -0
  31. package/lib/target/hosted-run/input.js +200 -0
  32. package/lib/target/hosted-run.d.ts +4 -0
  33. package/lib/target/hosted-run.js +12 -0
  34. package/lib/target/index.d.ts +6 -0
  35. package/lib/target/index.js +38 -0
  36. package/lib/target/target-registry.d.ts +45 -0
  37. package/lib/target/target-registry.js +132 -0
  38. package/lib/target/target-session.d.ts +40 -0
  39. package/lib/target/target-session.js +163 -0
  40. package/package.json +25 -7
  41. package/scripts/audit-production-dependencies.js +150 -0
  42. package/scripts/opcore-agent-gate.js +159 -0
  43. package/scripts/opcore-agent-tool-overlays.js +154 -0
  44. package/scripts/opcore-introduced-check.js +290 -0
  45. package/src/agent-cli-provider/adapters/codex.ts +1 -0
  46. package/src/isolation-manager.js +16 -1
  47. package/src/target/bounded-json.ts +48 -0
  48. package/src/target/credential-lock.ts +35 -0
  49. package/src/target/credential-store.ts +107 -0
  50. package/src/target/device-flow.ts +168 -0
  51. package/src/target/discovery.ts +131 -0
  52. package/src/target/hosted-run/client.ts +198 -0
  53. package/src/target/hosted-run/commands.ts +140 -0
  54. package/src/target/hosted-run/contracts.ts +53 -0
  55. package/src/target/hosted-run/input.ts +199 -0
  56. package/src/target/hosted-run.ts +8 -0
  57. package/src/target/index.ts +55 -0
  58. package/src/target/target-registry.ts +174 -0
  59. package/src/target/target-session.ts +253 -0
@@ -0,0 +1,168 @@
1
+ export interface DeviceCodeResponse {
2
+ readonly device_code: string;
3
+ readonly user_code: string;
4
+ readonly verification_uri: string;
5
+ readonly verification_uri_complete?: string;
6
+ readonly expires_in: number;
7
+ readonly interval: number;
8
+ }
9
+
10
+ export interface TokenResponse {
11
+ readonly access_token: string;
12
+ readonly refresh_token: string;
13
+ readonly token_type: string;
14
+ readonly expires_in: number;
15
+ readonly organization?: { readonly id: string; readonly name: string };
16
+ }
17
+
18
+ export interface HttpTransport {
19
+ fetch(url: string, init: RequestInit & { redirect: 'error' }): Promise<Response>;
20
+ }
21
+
22
+ export interface Clock {
23
+ now(): number;
24
+ }
25
+
26
+ export interface DeviceIdentity {
27
+ readonly token: string;
28
+ readonly label: string;
29
+ }
30
+
31
+ export class DeviceFlowDeniedError extends Error {
32
+ constructor() {
33
+ super('Device authorization denied by user');
34
+ this.name = 'DeviceFlowDeniedError';
35
+ }
36
+ }
37
+
38
+ export class DeviceFlowExpiredError extends Error {
39
+ constructor() {
40
+ super('Device authorization code expired');
41
+ this.name = 'DeviceFlowExpiredError';
42
+ }
43
+ }
44
+
45
+ export class UnboundSessionError extends Error {
46
+ readonly verificationUri: string;
47
+ constructor(verificationUri: string) {
48
+ super(
49
+ `Session not bound to an organization. Re-approve at ${verificationUri} and select an organization.`
50
+ );
51
+ this.name = 'UnboundSessionError';
52
+ this.verificationUri = verificationUri;
53
+ }
54
+ }
55
+
56
+ const DEFAULT_CLOCK: Clock = { now: () => Date.now() };
57
+
58
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
59
+ return new Promise((resolve, reject) => {
60
+ if (signal?.aborted) {
61
+ reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
62
+ return;
63
+ }
64
+ const timer = setTimeout(resolve, ms);
65
+ signal?.addEventListener(
66
+ 'abort',
67
+ () => {
68
+ clearTimeout(timer);
69
+ reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
70
+ },
71
+ { once: true }
72
+ );
73
+ });
74
+ }
75
+
76
+ export async function requestDeviceCode(
77
+ deviceAuthorizationEndpoint: string,
78
+ clientId: string,
79
+ http: HttpTransport,
80
+ signal?: AbortSignal
81
+ ): Promise<DeviceCodeResponse> {
82
+ const body = new URLSearchParams({
83
+ client_id: clientId,
84
+ scope: 'openid',
85
+ });
86
+
87
+ const init: RequestInit & { redirect: 'error' } = {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
90
+ body: body.toString(),
91
+ redirect: 'error',
92
+ };
93
+ if (signal) init.signal = signal;
94
+
95
+ const response = await http.fetch(deviceAuthorizationEndpoint, init);
96
+
97
+ if (!response.ok) {
98
+ const text = await response.text();
99
+ throw new Error(`Device code request failed (${response.status}): ${text}`);
100
+ }
101
+
102
+ return (await response.json()) as DeviceCodeResponse;
103
+ }
104
+
105
+ export async function pollForToken(
106
+ tokenEndpoint: string,
107
+ clientId: string,
108
+ deviceCode: string,
109
+ interval: number,
110
+ expiresIn: number,
111
+ http: HttpTransport,
112
+ clock: Clock = DEFAULT_CLOCK,
113
+ signal?: AbortSignal,
114
+ deviceIdentity?: DeviceIdentity
115
+ ): Promise<TokenResponse> {
116
+ const deadline = clock.now() + expiresIn * 1000;
117
+ let currentInterval = interval;
118
+
119
+ while (clock.now() < deadline) {
120
+ if (signal?.aborted) {
121
+ throw signal.reason ?? new DOMException('Aborted', 'AbortError');
122
+ }
123
+
124
+ await sleep(currentInterval * 1000, signal);
125
+
126
+ const body = new URLSearchParams({
127
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
128
+ device_code: deviceCode,
129
+ client_id: clientId,
130
+ });
131
+ if (deviceIdentity) {
132
+ body.set('audience', 'admin');
133
+ body.set('device_token', deviceIdentity.token);
134
+ body.set('device_label', deviceIdentity.label);
135
+ }
136
+
137
+ const init: RequestInit & { redirect: 'error' } = {
138
+ method: 'POST',
139
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
140
+ body: body.toString(),
141
+ redirect: 'error',
142
+ };
143
+ if (signal) init.signal = signal;
144
+
145
+ const response = await http.fetch(tokenEndpoint, init);
146
+
147
+ if (response.ok) {
148
+ return (await response.json()) as TokenResponse;
149
+ }
150
+
151
+ const errorBody = (await response.json()) as { error: string };
152
+ switch (errorBody.error) {
153
+ case 'authorization_pending':
154
+ continue;
155
+ case 'slow_down':
156
+ currentInterval += 5;
157
+ continue;
158
+ case 'access_denied':
159
+ throw new DeviceFlowDeniedError();
160
+ case 'expired_token':
161
+ throw new DeviceFlowExpiredError();
162
+ default:
163
+ throw new Error(`Token endpoint error: ${errorBody.error}`);
164
+ }
165
+ }
166
+
167
+ throw new DeviceFlowExpiredError();
168
+ }
@@ -0,0 +1,131 @@
1
+ import type { HttpTransport } from './device-flow.ts';
2
+ import { readBoundedJson } from './bounded-json.ts';
3
+
4
+ const DISCOVERY_PATH = '/.well-known/openengine-hosted-target';
5
+ const MAX_DISCOVERY_BYTES = 64 * 1024;
6
+
7
+ export interface TargetSessionEndpoints {
8
+ readonly deviceAuthorizationEndpoint: string;
9
+ readonly tokenEndpoint: string;
10
+ readonly revocationEndpoint?: string;
11
+ readonly clientId: string;
12
+ readonly capsuleApiBaseUrl: string;
13
+ }
14
+
15
+ export class TargetDiscoveryError extends Error {
16
+ constructor(message: string) {
17
+ super(`Target discovery failed: ${message}`);
18
+ this.name = 'TargetDiscoveryError';
19
+ }
20
+ }
21
+
22
+ function record(value: unknown, field: string): Record<string, unknown> {
23
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
24
+ throw new TargetDiscoveryError(`${field} must be an object`);
25
+ }
26
+ return value as Record<string, unknown>;
27
+ }
28
+
29
+ function stringField(source: Record<string, unknown>, field: string): string {
30
+ const value = source[field];
31
+ if (typeof value !== 'string' || value.length === 0) {
32
+ throw new TargetDiscoveryError(`${field} must be a non-empty string`);
33
+ }
34
+ return value;
35
+ }
36
+
37
+ function safeEndpoint(value: unknown, field: string, serviceOrigin: string): string {
38
+ if (typeof value !== 'string') {
39
+ throw new TargetDiscoveryError(`${field} must be an absolute URL`);
40
+ }
41
+ let endpoint: URL;
42
+ try {
43
+ endpoint = new URL(value);
44
+ } catch {
45
+ throw new TargetDiscoveryError(`${field} must be an absolute URL`);
46
+ }
47
+ if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
48
+ throw new TargetDiscoveryError(`${field} contains forbidden URL components`);
49
+ }
50
+ if (endpoint.origin !== serviceOrigin) {
51
+ throw new TargetDiscoveryError(`${field} must remain on the target origin`);
52
+ }
53
+ return endpoint.href;
54
+ }
55
+
56
+ async function fetchDocument(http: HttpTransport, url: string): Promise<Record<string, unknown>> {
57
+ const response = await http.fetch(url, {
58
+ method: 'GET',
59
+ headers: { Accept: 'application/json' },
60
+ redirect: 'error',
61
+ });
62
+ if (!response.ok) {
63
+ throw new TargetDiscoveryError(`request failed with status ${response.status}`);
64
+ }
65
+ const body = await readBoundedJson(response, MAX_DISCOVERY_BYTES, {
66
+ tooLarge: () => new TargetDiscoveryError('response exceeds the size limit'),
67
+ invalid: () => new TargetDiscoveryError('response is not valid JSON'),
68
+ });
69
+ return record(body, 'response');
70
+ }
71
+
72
+ export async function discoverTargetSessionEndpoints(
73
+ targetUrl: string,
74
+ http: HttpTransport
75
+ ): Promise<TargetSessionEndpoints> {
76
+ const target = new URL(targetUrl);
77
+ const discovery = await fetchDocument(http, new URL(DISCOVERY_PATH, target).href);
78
+ if (discovery.kind !== 'openengine.hosted-target/v1') {
79
+ throw new TargetDiscoveryError('unsupported hosted-target version');
80
+ }
81
+ if (discovery.organization_binding !== 'device_approval') {
82
+ throw new TargetDiscoveryError('unsupported organization binding');
83
+ }
84
+
85
+ const oauth = record(discovery.oauth, 'oauth');
86
+ const capsuleProtocol = record(discovery.capsule_protocol, 'capsule_protocol');
87
+ if (capsuleProtocol.name !== 'openengine.capsules/v1' || capsuleProtocol.major_version !== 1) {
88
+ throw new TargetDiscoveryError('unsupported capsule protocol');
89
+ }
90
+ const capsuleApiBaseUrl = safeEndpoint(
91
+ capsuleProtocol.base_url,
92
+ 'capsule_protocol.base_url',
93
+ target.origin
94
+ ).replace(/\/$/, '');
95
+ const metadataUrl = safeEndpoint(oauth.metadata_url, 'oauth.metadata_url', target.origin);
96
+ const deviceEndpoint = safeEndpoint(
97
+ oauth.device_authorization_endpoint,
98
+ 'oauth.device_authorization_endpoint',
99
+ target.origin
100
+ );
101
+ const tokenEndpoint = safeEndpoint(oauth.token_endpoint, 'oauth.token_endpoint', target.origin);
102
+ const clientId = stringField(oauth, 'client_id');
103
+
104
+ const metadata = await fetchDocument(http, metadataUrl);
105
+ const metadataDeviceEndpoint = safeEndpoint(
106
+ metadata.device_authorization_endpoint,
107
+ 'device_authorization_endpoint',
108
+ target.origin
109
+ );
110
+ const metadataTokenEndpoint = safeEndpoint(
111
+ metadata.token_endpoint,
112
+ 'token_endpoint',
113
+ target.origin
114
+ );
115
+ if (metadataDeviceEndpoint !== deviceEndpoint || metadataTokenEndpoint !== tokenEndpoint) {
116
+ throw new TargetDiscoveryError('OAuth metadata does not match hosted-target discovery');
117
+ }
118
+
119
+ const revocationEndpoint =
120
+ metadata.revocation_endpoint === undefined
121
+ ? undefined
122
+ : safeEndpoint(metadata.revocation_endpoint, 'revocation_endpoint', target.origin);
123
+
124
+ return {
125
+ deviceAuthorizationEndpoint: deviceEndpoint,
126
+ tokenEndpoint,
127
+ ...(revocationEndpoint === undefined ? {} : { revocationEndpoint }),
128
+ clientId,
129
+ capsuleApiBaseUrl,
130
+ };
131
+ }
@@ -0,0 +1,198 @@
1
+ import { acquireTargetLock } from '../credential-lock.ts';
2
+ import { KeyringCredentialStore } from '../credential-store.ts';
3
+ import { discoverTargetSessionEndpoints } from '../discovery.ts';
4
+ import { getTarget } from '../target-registry.ts';
5
+ import { getAccessTokenProvider, type TargetAccessTokenProvider } from '../target-session.ts';
6
+ import { readBoundedJson } from '../bounded-json.ts';
7
+
8
+ import type { HostedRunDependencies, HostedRunIntent } from './contracts.ts';
9
+
10
+ const MAX_RESPONSE_BYTES = 1024 * 1024;
11
+ export const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
+ export const TERMINAL_STATES = new Set(['succeeded', 'failed', 'cancelled', 'expired']);
13
+ const RUN_INTENT_STATES = new Set([
14
+ 'queued',
15
+ 'provisioning',
16
+ 'running',
17
+ 'cancelling',
18
+ ...TERMINAL_STATES,
19
+ ]);
20
+
21
+ export interface HostedContext {
22
+ readonly targetName: string;
23
+ readonly organization: string;
24
+ readonly client: RunIntentClient;
25
+ }
26
+
27
+ function wait(milliseconds: number): Promise<void> {
28
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
29
+ }
30
+
31
+ export class HostedRunHttpError extends Error {
32
+ readonly status: number;
33
+ readonly code: string | null;
34
+
35
+ constructor(status: number, message: string, code: string | null = null) {
36
+ super(message);
37
+ this.name = 'HostedRunHttpError';
38
+ this.status = status;
39
+ this.code = code;
40
+ }
41
+ }
42
+
43
+ function validateIntent(value: unknown): HostedRunIntent {
44
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
45
+ throw new Error('Zero Cloud returned an invalid run intent');
46
+ }
47
+ const record = value as Record<string, unknown>;
48
+ if (
49
+ typeof record['intent_id'] !== 'string' ||
50
+ !UUID.test(record['intent_id']) ||
51
+ typeof record['state'] !== 'string' ||
52
+ !RUN_INTENT_STATES.has(record['state'])
53
+ ) {
54
+ throw new Error('Zero Cloud returned an invalid run intent');
55
+ }
56
+ const result = record['result'];
57
+ if (result !== null && (typeof result !== 'object' || Array.isArray(result))) {
58
+ throw new Error('Zero Cloud returned an invalid run intent result');
59
+ }
60
+ return record as unknown as HostedRunIntent;
61
+ }
62
+
63
+ export class RunIntentClient {
64
+ private readonly baseUrl: string;
65
+ private readonly organization: string;
66
+ private readonly tokenProvider: TargetAccessTokenProvider;
67
+ private readonly fetch: typeof globalThis.fetch;
68
+
69
+ constructor(
70
+ baseUrl: string,
71
+ organization: string,
72
+ tokenProvider: TargetAccessTokenProvider,
73
+ fetchImplementation: typeof globalThis.fetch
74
+ ) {
75
+ this.baseUrl = baseUrl.replace(/\/$/, '');
76
+ this.organization = organization;
77
+ this.tokenProvider = tokenProvider;
78
+ this.fetch = fetchImplementation;
79
+ }
80
+
81
+ async submit(body: Record<string, unknown>, submissionKey: string): Promise<HostedRunIntent> {
82
+ let failure: unknown;
83
+ for (let attempt = 0; attempt < 2; attempt += 1) {
84
+ try {
85
+ return await this.request('', {
86
+ method: 'POST',
87
+ body: JSON.stringify(body),
88
+ headers: { 'Idempotency-Key': submissionKey },
89
+ });
90
+ } catch (error) {
91
+ failure = error;
92
+ if (error instanceof HostedRunHttpError && error.status < 500) throw error;
93
+ if (attempt === 0) await wait(250);
94
+ }
95
+ }
96
+ throw failure;
97
+ }
98
+
99
+ get(intentId: string): Promise<HostedRunIntent> {
100
+ return this.request(`/${encodeURIComponent(intentId)}`, { method: 'GET' });
101
+ }
102
+
103
+ cancel(intentId: string): Promise<HostedRunIntent> {
104
+ return this.request(`/${encodeURIComponent(intentId)}`, { method: 'DELETE' });
105
+ }
106
+
107
+ private async request(
108
+ suffix: string,
109
+ options: { method: string; body?: string; headers?: Record<string, string> }
110
+ ): Promise<HostedRunIntent> {
111
+ const token = await this.tokenProvider.getAccessToken();
112
+ const url = `${this.baseUrl}/orgs/${encodeURIComponent(this.organization)}/run-intents${suffix}`;
113
+ const init: RequestInit & { redirect: 'error' } = {
114
+ method: options.method,
115
+ redirect: 'error',
116
+ headers: {
117
+ Accept: 'application/json',
118
+ Authorization: `Bearer ${token}`,
119
+ 'Content-Type': 'application/json',
120
+ ...options.headers,
121
+ },
122
+ };
123
+ if (options.body !== undefined) init.body = options.body;
124
+ let response: Response;
125
+ try {
126
+ response = await this.fetch(url, init);
127
+ } catch (error) {
128
+ throw new Error(
129
+ `Zero Cloud request failed: ${error instanceof Error ? error.message : String(error)}`
130
+ );
131
+ }
132
+ const body = await readBoundedJson(response, MAX_RESPONSE_BYTES, {
133
+ tooLarge: () => new Error('Zero Cloud response exceeded 1 MiB'),
134
+ invalid: () => new Error('Zero Cloud returned invalid JSON'),
135
+ });
136
+ if (!response.ok) {
137
+ const problem = body as Record<string, unknown>;
138
+ const code = typeof problem['code'] === 'string' ? problem['code'] : null;
139
+ const message =
140
+ typeof problem['message'] === 'string' ? problem['message'] : 'request failed';
141
+ throw new HostedRunHttpError(
142
+ response.status,
143
+ `Zero Cloud ${response.status}: ${message}`,
144
+ code
145
+ );
146
+ }
147
+ return validateIntent(body);
148
+ }
149
+ }
150
+
151
+ export async function context(targetName: string, deps: HostedRunDependencies): Promise<HostedContext> {
152
+ const environment = deps.environment ?? process.env;
153
+ const target = getTarget(targetName, deps.settings);
154
+ if (!target) throw new Error(`Target "${targetName}" not found.`);
155
+ const http = {
156
+ fetch: (url: string, init: RequestInit & { redirect: 'error' }) =>
157
+ (deps.fetch ?? globalThis.fetch)(url, init),
158
+ };
159
+ const discovery = await discoverTargetSessionEndpoints(target.url, http);
160
+ const ephemeralToken = environment['ZEROSHOT_TARGET_ACCESS_TOKEN']?.trim();
161
+ const ephemeralOrganization = environment['ZEROSHOT_TARGET_ORGANIZATION']?.trim();
162
+ if ((ephemeralToken && !ephemeralOrganization) || (!ephemeralToken && ephemeralOrganization)) {
163
+ throw new Error(
164
+ 'ZEROSHOT_TARGET_ACCESS_TOKEN and ZEROSHOT_TARGET_ORGANIZATION must be provided together'
165
+ );
166
+ }
167
+
168
+ let organization: string;
169
+ let tokenProvider: TargetAccessTokenProvider;
170
+ if (ephemeralToken && ephemeralOrganization) {
171
+ organization = ephemeralOrganization;
172
+ tokenProvider = { getAccessToken: async () => ephemeralToken };
173
+ } else {
174
+ if (!target.organization) {
175
+ throw new Error(`Login required. Run: zeroshot target login ${targetName}`);
176
+ }
177
+ organization = target.organization.id;
178
+ const credentials = await KeyringCredentialStore.create();
179
+ tokenProvider = getAccessTokenProvider(
180
+ targetName,
181
+ target,
182
+ credentials,
183
+ () => acquireTargetLock(target.id),
184
+ { http, discoveryEndpoints: discovery }
185
+ );
186
+ }
187
+
188
+ return {
189
+ targetName,
190
+ organization,
191
+ client: new RunIntentClient(
192
+ discovery.capsuleApiBaseUrl,
193
+ organization,
194
+ tokenProvider,
195
+ deps.fetch ?? globalThis.fetch
196
+ ),
197
+ };
198
+ }
@@ -0,0 +1,140 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ import type { HostedOptions, HostedRunDependencies, HostedRunIntent } from './contracts.ts';
4
+ import {
5
+ context,
6
+ HostedRunHttpError,
7
+ TERMINAL_STATES,
8
+ UUID,
9
+ type HostedContext,
10
+ } from './client.ts';
11
+ import {
12
+ githubToken,
13
+ providerKey,
14
+ resolveHostedInput,
15
+ validateHostedOptions,
16
+ } from './input.ts';
17
+
18
+ const DEFAULT_MODEL = 'openai/gpt-5.4';
19
+ const RUN_INTENT_VERSION = 'zeroshot.run-intent/v1';
20
+ const MAX_RUN_INTENT_BYTES = 1024 * 1024 + 64 * 1024;
21
+ const RUN_INTENT_POLL_MS = 500;
22
+
23
+ function output(deps: HostedRunDependencies, value: string): void {
24
+ (deps.stdout ?? process.stdout).write(`${value}\n`);
25
+ }
26
+
27
+ function wait(milliseconds: number): Promise<void> {
28
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
29
+ }
30
+
31
+ function displayState(intent: HostedRunIntent): string {
32
+ return intent.waiting_reason ? `${intent.state} (${intent.waiting_reason})` : intent.state;
33
+ }
34
+
35
+ function resumeCommand(value: HostedContext, intentId: string): string {
36
+ return `zeroshot target status ${value.targetName} ${intentId} --follow`;
37
+ }
38
+
39
+ async function follow(
40
+ value: HostedContext,
41
+ initial: HostedRunIntent,
42
+ deps: HostedRunDependencies
43
+ ): Promise<Record<string, unknown> | null> {
44
+ let intent = initial;
45
+ let displayed: string | null = null;
46
+ for (;;) {
47
+ const state = displayState(intent);
48
+ if (state !== displayed) {
49
+ output(deps, `Run ${intent.intent_id}: ${state}`);
50
+ displayed = state;
51
+ }
52
+ if (TERMINAL_STATES.has(intent.state)) break;
53
+ await (deps.delay ?? wait)(RUN_INTENT_POLL_MS);
54
+ intent = await value.client.get(intent.intent_id);
55
+ }
56
+ if (intent.state === 'succeeded') {
57
+ const summary = intent.result?.['summary'];
58
+ if (typeof summary === 'string' && summary) output(deps, summary);
59
+ return intent.result;
60
+ }
61
+ const detail = intent.error_code ? ` (${intent.error_code})` : '';
62
+ throw new Error(`hosted run ${intent.state}${detail}`);
63
+ }
64
+
65
+ export async function runHosted(
66
+ input: string,
67
+ options: HostedOptions,
68
+ deps: HostedRunDependencies
69
+ ): Promise<HostedRunIntent | Record<string, unknown> | null> {
70
+ validateHostedOptions(options);
71
+ const environment = deps.environment ?? process.env;
72
+ const resolved = await resolveHostedInput(input, options, environment);
73
+ const value = await context(options.target!, deps);
74
+ const body = {
75
+ label: 'zeroshot-cli',
76
+ size: options.size ?? 'standard',
77
+ intent: {
78
+ version: RUN_INTENT_VERSION,
79
+ credentials: {
80
+ githubToken: githubToken(environment),
81
+ openrouterApiKey: providerKey(environment),
82
+ repository: resolved.repository,
83
+ model: options.model ?? DEFAULT_MODEL,
84
+ },
85
+ request: resolved.request,
86
+ },
87
+ };
88
+ if (Buffer.byteLength(JSON.stringify(body)) > MAX_RUN_INTENT_BYTES) {
89
+ throw new Error('hosted run intent exceeds the 1088 KiB upload limit');
90
+ }
91
+ const submissionKey = options.submissionKey ?? crypto.randomUUID();
92
+ let created: HostedRunIntent;
93
+ try {
94
+ created = await value.client.submit(body, submissionKey);
95
+ } catch (error) {
96
+ if (error instanceof HostedRunHttpError) throw error;
97
+ throw new Error(
98
+ `${error instanceof Error ? error.message : String(error)}. Recover this submission by rerunning ` +
99
+ `the same command with --submission-key ${submissionKey}`,
100
+ { cause: error }
101
+ );
102
+ }
103
+ output(deps, `Run ${created.intent_id} queued`);
104
+ output(deps, `Resume: ${resumeCommand(value, created.intent_id)}`);
105
+ if (options.detach) return created;
106
+ output(deps, 'Ctrl+C disconnects without cancelling.');
107
+ return follow(value, created, deps);
108
+ }
109
+
110
+ export async function statusHostedRun(
111
+ targetName: string,
112
+ intentId: string,
113
+ shouldFollow: boolean,
114
+ deps: HostedRunDependencies
115
+ ): Promise<HostedRunIntent | Record<string, unknown> | null> {
116
+ if (!UUID.test(intentId)) throw new Error('run intent id must be a UUID');
117
+ const value = await context(targetName, deps);
118
+ const intent = await value.client.get(intentId);
119
+ if (!shouldFollow) {
120
+ output(deps, JSON.stringify(intent, null, 2));
121
+ return intent;
122
+ }
123
+ if (!TERMINAL_STATES.has(intent.state)) {
124
+ output(deps, `Following ${intentId}; Ctrl+C disconnects without cancelling.`);
125
+ output(deps, `Resume: ${resumeCommand(value, intentId)}`);
126
+ }
127
+ return follow(value, intent, deps);
128
+ }
129
+
130
+ export async function cancelHostedRun(
131
+ targetName: string,
132
+ intentId: string,
133
+ deps: HostedRunDependencies
134
+ ): Promise<HostedRunIntent> {
135
+ if (!UUID.test(intentId)) throw new Error('run intent id must be a UUID');
136
+ const value = await context(targetName, deps);
137
+ const intent = await value.client.cancel(intentId);
138
+ output(deps, `Run ${intent.intent_id}: ${displayState(intent)}`);
139
+ return intent;
140
+ }
@@ -0,0 +1,53 @@
1
+ // Shared hosted-run command contracts.
2
+
3
+ import type { SettingsPort } from '../target-registry.ts';
4
+
5
+ export interface HostedOptions {
6
+ readonly target?: string;
7
+ readonly repository?: string;
8
+ readonly model?: string;
9
+ readonly size?: string;
10
+ readonly submissionKey?: string;
11
+ readonly detach?: boolean;
12
+ readonly pr?: boolean;
13
+ readonly provider?: string;
14
+ readonly config?: string;
15
+ readonly docker?: boolean;
16
+ readonly worktree?: boolean;
17
+ readonly dockerImage?: string;
18
+ readonly strictSchema?: boolean;
19
+ readonly ship?: boolean;
20
+ readonly prBase?: string;
21
+ readonly mergeQueue?: boolean;
22
+ readonly closeIssue?: string;
23
+ readonly workers?: number;
24
+ readonly gitlab?: boolean;
25
+ readonly jira?: boolean;
26
+ readonly devops?: boolean;
27
+ readonly linear?: boolean;
28
+ readonly mount?: readonly string[];
29
+ readonly noMounts?: boolean;
30
+ readonly containerHome?: string;
31
+ }
32
+
33
+ export interface HostedRunIntent {
34
+ readonly intent_id: string;
35
+ readonly state: string;
36
+ readonly waiting_reason: string | null;
37
+ readonly result: Record<string, unknown> | null;
38
+ readonly error_code: string | null;
39
+ readonly [key: string]: unknown;
40
+ }
41
+
42
+ export interface HostedRunDependencies {
43
+ readonly settings: SettingsPort;
44
+ readonly environment?: NodeJS.ProcessEnv;
45
+ readonly fetch?: typeof globalThis.fetch;
46
+ readonly delay?: (milliseconds: number) => Promise<void>;
47
+ readonly stdout?: { write(value: string): void };
48
+ }
49
+
50
+ export interface ResolvedInput {
51
+ readonly repository: string;
52
+ readonly request: Record<string, unknown>;
53
+ }