orizu 0.5.8 → 0.5.10

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.
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Hosted-loop LIFECYCLE seam (ALI-1015 Phase 1, per ADR-005 Resolution).
3
+ *
4
+ * The orchestration-facing parts of the hosted loop, extracted VERBATIM from
5
+ * `hosted-loop.ts` so they can be re-exported through the shared
6
+ * `lib/hosted-runtime/` surface and driven by a server-side coordinator (the
7
+ * per-session Durable Object) as well as the CLI. This module deliberately
8
+ * carries NO harness/runtime machinery: its only import is the TYPE-ONLY
9
+ * `HarnessEvent` (erased at compile time), so a Worker bundle that pulls the
10
+ * lifecycle seam never drags in the OpenCode/Claude-SDK drivers or any Node
11
+ * child-process code.
12
+ *
13
+ * Contents (moved, not rewritten — behavior identical):
14
+ * - `HostedLoopContext` — the run-scoped boot context the host orchestrator
15
+ * writes for the in-sandbox loop;
16
+ * - `buildOpenCodeSpawnEnv` — the pre-authenticated agent env (ALI-1044);
17
+ * - the G5 egress canary: `runEgressCanary`, `egressCanaryAllowedHost`,
18
+ * `defaultProbeEgress`, and their types/constants;
19
+ * - `SetupHookOutcome` — the deferred customer-setup-hook contract.
20
+ *
21
+ * `hosted-loop.ts` re-exports everything here, so the CLI's import surface
22
+ * (`@/packages/cli/src/hosted-loop`) is unchanged.
23
+ */
24
+ /**
25
+ * Environment injected into the spawned OpenCode server so the AGENT's bash sees a
26
+ * PRE-AUTHENTICATED `orizu` CLI (ALI-1044). ORIZU_TOKEN_FILE points at the 0600
27
+ * rotated bearer file and ORIZU_BASE_URL at the Orizu API — the exact vars the
28
+ * CLI's auth resolution reads (resolveEnvBearerToken + resolveBaseUrl). The dummy
29
+ * ANTHROPIC key (model-key brokering, G3) is preserved. No real secret is carried
30
+ * here: the bearer stays on disk and is read fresh per request from the file.
31
+ */
32
+ export function buildOpenCodeSpawnEnv(context) {
33
+ const env = {};
34
+ if (context.anthropicDummyKey) {
35
+ env.ANTHROPIC_API_KEY = context.anthropicDummyKey;
36
+ }
37
+ if (context.bearerFile) {
38
+ env.ORIZU_TOKEN_FILE = context.bearerFile;
39
+ }
40
+ if (context.apiBaseUrl) {
41
+ env.ORIZU_BASE_URL = context.apiBaseUrl;
42
+ }
43
+ return Object.keys(env).length > 0 ? env : undefined;
44
+ }
45
+ /** Default egress-canary probe host (a known non-allowlisted destination). */
46
+ export const DEFAULT_EGRESS_CANARY_HOST = 'example.com';
47
+ /** Bound (ms) on the canary probe — a live firewall resets/times out fast; a
48
+ * reachable host answers well within this. */
49
+ const EGRESS_CANARY_TIMEOUT_MS = 5000;
50
+ /**
51
+ * Default probe: attempt a bounded HTTPS GET to `host`. A live default-deny
52
+ * firewall makes this THROW for a DENIED host (connection reset / timeout / DNS
53
+ * failure) → `reachable: false`. Any response at all — even an error status —
54
+ * proves the host was reachable → `reachable: true`. Used for BOTH the allowed
55
+ * (positive-control) and denied probes.
56
+ */
57
+ export async function defaultProbeEgress(host) {
58
+ const controller = new AbortController();
59
+ const timer = setTimeout(() => controller.abort(), EGRESS_CANARY_TIMEOUT_MS);
60
+ try {
61
+ const response = await fetch(`https://${host}/`, {
62
+ method: 'GET',
63
+ redirect: 'manual',
64
+ signal: controller.signal,
65
+ });
66
+ return { reachable: true, detail: `reachable (HTTP ${response.status})` };
67
+ }
68
+ catch (error) {
69
+ return { reachable: false, detail: `blocked (${error instanceof Error ? error.name : 'error'})` };
70
+ }
71
+ finally {
72
+ clearTimeout(timer);
73
+ }
74
+ }
75
+ /**
76
+ * Run the startup egress canary as a POSITIVE CONTROL and emit the proof event
77
+ * through the (single-writer) sink. Probes BOTH a known-allowed host and the
78
+ * known-denied host, then decides:
79
+ *
80
+ * - allowed REACHABLE and denied BLOCKED → the firewall is live AND the network
81
+ * works → emit `egress_blocked` and PROCEED (the ONLY healthy outcome);
82
+ * - denied REACHABLE → egress is NOT enforced → emit `egress_allowed`, FAIL
83
+ * CLOSED;
84
+ * - allowed ALSO UNREACHABLE (denied blocked but the positive control failed) →
85
+ * we cannot tell policy-blocking from a total network outage → emit
86
+ * `egress_allowed`, FAIL CLOSED.
87
+ *
88
+ * The `egress_blocked` kind therefore means EXACTLY "proceeded, positive control
89
+ * passed"; `egress_allowed` means "failed closed" (the payload `result` field
90
+ * distinguishes an unexpectedly-reachable denied host from an unreachable
91
+ * positive control). Old behavior — treating ANY denied-host throw (incl. a
92
+ * timeout under a total outage) as "blocked → proceed" — failed OPEN; this now
93
+ * fails closed on that ambiguity.
94
+ */
95
+ export async function runEgressCanary(targets, sink, probe, now = () => Date.now()) {
96
+ const [allowed, denied] = await Promise.all([probe(targets.allowedHost), probe(targets.deniedHost)]);
97
+ const checkedAt = new Date(now()).toISOString();
98
+ const positiveControl = {
99
+ host: targets.allowedHost,
100
+ reachable: allowed.reachable,
101
+ detail: allowed.detail,
102
+ };
103
+ // FAIL CLOSED: a denied host answered — the policy is not enforcing egress.
104
+ if (denied.reachable) {
105
+ await sink.append({
106
+ kind: 'egress_allowed',
107
+ critical: true,
108
+ payload: {
109
+ host: targets.deniedHost,
110
+ result: 'unexpectedly_reachable',
111
+ detail: denied.detail,
112
+ positiveControl,
113
+ checkedAt,
114
+ },
115
+ });
116
+ return {
117
+ proceed: false,
118
+ reason: `denied host ${targets.deniedHost} was reachable (${denied.detail}) — egress is not enforced`,
119
+ allowed,
120
+ denied,
121
+ };
122
+ }
123
+ // FAIL CLOSED: the denied host was blocked, but the positive control ALSO did
124
+ // not answer — the environment/network is broken and we cannot distinguish a
125
+ // policy block from a total outage. This is the case the old code got wrong.
126
+ if (!allowed.reachable) {
127
+ await sink.append({
128
+ kind: 'egress_allowed',
129
+ critical: true,
130
+ payload: {
131
+ host: targets.deniedHost,
132
+ result: 'positive_control_unreachable',
133
+ detail: denied.detail,
134
+ positiveControl,
135
+ checkedAt,
136
+ },
137
+ });
138
+ return {
139
+ proceed: false,
140
+ reason: `positive control ${targets.allowedHost} was unreachable (${allowed.detail}) — cannot distinguish egress enforcement from a network outage`,
141
+ allowed,
142
+ denied,
143
+ };
144
+ }
145
+ // HEALTHY: allowed reachable AND denied blocked → the firewall is live.
146
+ await sink.append({
147
+ kind: 'egress_blocked',
148
+ critical: true,
149
+ payload: {
150
+ host: targets.deniedHost,
151
+ result: 'blocked',
152
+ detail: denied.detail,
153
+ positiveControl,
154
+ checkedAt,
155
+ },
156
+ });
157
+ return { proceed: true, reason: null, allowed, denied };
158
+ }
159
+ /**
160
+ * Derive the known-ALLOWED positive-control host from the run's Orizu API base —
161
+ * always allowlisted (it is the agent's control-plane path) and ours, so it is
162
+ * the safest "must be reachable" signal. Falls back to a lenient host parse if
163
+ * the base is not a well-formed URL.
164
+ */
165
+ export function egressCanaryAllowedHost(apiBaseUrl) {
166
+ try {
167
+ return new URL(apiBaseUrl).hostname;
168
+ }
169
+ catch {
170
+ return apiBaseUrl.replace(/^[a-z]+:\/\//i, '').split('/')[0].split(':')[0];
171
+ }
172
+ }
@@ -42,155 +42,11 @@ import { annotateHeadlessQuestions, withIdleWatchdog } from './hosted-headless.j
42
42
  import { OPENCODE_PINNED_VERSION, createOpenCodeHarness, spawnOpenCode, } from './hosted-harness-opencode.js';
43
43
  import { createClaudeAgentHarness } from './hosted-harness-claude.js';
44
44
  import { drainHarnessToSink, resumeRunEventSink, } from './hosted-run-event-sink.js';
45
- /**
46
- * Environment injected into the spawned OpenCode server so the AGENT's bash sees a
47
- * PRE-AUTHENTICATED `orizu` CLI (ALI-1044). ORIZU_TOKEN_FILE points at the 0600
48
- * rotated bearer file and ORIZU_BASE_URL at the Orizu API — the exact vars the
49
- * CLI's auth resolution reads (resolveEnvBearerToken + resolveBaseUrl). The dummy
50
- * ANTHROPIC key (model-key brokering, G3) is preserved. No real secret is carried
51
- * here: the bearer stays on disk and is read fresh per request from the file.
52
- */
53
- export function buildOpenCodeSpawnEnv(context) {
54
- const env = {};
55
- if (context.anthropicDummyKey) {
56
- env.ANTHROPIC_API_KEY = context.anthropicDummyKey;
57
- }
58
- if (context.bearerFile) {
59
- env.ORIZU_TOKEN_FILE = context.bearerFile;
60
- }
61
- if (context.apiBaseUrl) {
62
- env.ORIZU_BASE_URL = context.apiBaseUrl;
63
- }
64
- return Object.keys(env).length > 0 ? env : undefined;
65
- }
66
- /** Default egress-canary probe host (a known non-allowlisted destination). */
67
- export const DEFAULT_EGRESS_CANARY_HOST = 'example.com';
68
- /** Bound (ms) on the canary probe — a live firewall resets/times out fast; a
69
- * reachable host answers well within this. */
70
- const EGRESS_CANARY_TIMEOUT_MS = 5000;
71
- /**
72
- * Default probe: attempt a bounded HTTPS GET to `host`. A live default-deny
73
- * firewall makes this THROW for a DENIED host (connection reset / timeout / DNS
74
- * failure) → `reachable: false`. Any response at all — even an error status —
75
- * proves the host was reachable → `reachable: true`. Used for BOTH the allowed
76
- * (positive-control) and denied probes.
77
- */
78
- async function defaultProbeEgress(host) {
79
- const controller = new AbortController();
80
- const timer = setTimeout(() => controller.abort(), EGRESS_CANARY_TIMEOUT_MS);
81
- try {
82
- const response = await fetch(`https://${host}/`, {
83
- method: 'GET',
84
- redirect: 'manual',
85
- signal: controller.signal,
86
- });
87
- return { reachable: true, detail: `reachable (HTTP ${response.status})` };
88
- }
89
- catch (error) {
90
- return { reachable: false, detail: `blocked (${error instanceof Error ? error.name : 'error'})` };
91
- }
92
- finally {
93
- clearTimeout(timer);
94
- }
95
- }
96
- /**
97
- * Run the startup egress canary as a POSITIVE CONTROL and emit the proof event
98
- * through the (single-writer) sink. Probes BOTH a known-allowed host and the
99
- * known-denied host, then decides:
100
- *
101
- * - allowed REACHABLE and denied BLOCKED → the firewall is live AND the network
102
- * works → emit `egress_blocked` and PROCEED (the ONLY healthy outcome);
103
- * - denied REACHABLE → egress is NOT enforced → emit `egress_allowed`, FAIL
104
- * CLOSED;
105
- * - allowed ALSO UNREACHABLE (denied blocked but the positive control failed) →
106
- * we cannot tell policy-blocking from a total network outage → emit
107
- * `egress_allowed`, FAIL CLOSED.
108
- *
109
- * The `egress_blocked` kind therefore means EXACTLY "proceeded, positive control
110
- * passed"; `egress_allowed` means "failed closed" (the payload `result` field
111
- * distinguishes an unexpectedly-reachable denied host from an unreachable
112
- * positive control). Old behavior — treating ANY denied-host throw (incl. a
113
- * timeout under a total outage) as "blocked → proceed" — failed OPEN; this now
114
- * fails closed on that ambiguity.
115
- */
116
- export async function runEgressCanary(targets, sink, probe, now = () => Date.now()) {
117
- const [allowed, denied] = await Promise.all([probe(targets.allowedHost), probe(targets.deniedHost)]);
118
- const checkedAt = new Date(now()).toISOString();
119
- const positiveControl = {
120
- host: targets.allowedHost,
121
- reachable: allowed.reachable,
122
- detail: allowed.detail,
123
- };
124
- // FAIL CLOSED: a denied host answered — the policy is not enforcing egress.
125
- if (denied.reachable) {
126
- await sink.append({
127
- kind: 'egress_allowed',
128
- critical: true,
129
- payload: {
130
- host: targets.deniedHost,
131
- result: 'unexpectedly_reachable',
132
- detail: denied.detail,
133
- positiveControl,
134
- checkedAt,
135
- },
136
- });
137
- return {
138
- proceed: false,
139
- reason: `denied host ${targets.deniedHost} was reachable (${denied.detail}) — egress is not enforced`,
140
- allowed,
141
- denied,
142
- };
143
- }
144
- // FAIL CLOSED: the denied host was blocked, but the positive control ALSO did
145
- // not answer — the environment/network is broken and we cannot distinguish a
146
- // policy block from a total outage. This is the case the old code got wrong.
147
- if (!allowed.reachable) {
148
- await sink.append({
149
- kind: 'egress_allowed',
150
- critical: true,
151
- payload: {
152
- host: targets.deniedHost,
153
- result: 'positive_control_unreachable',
154
- detail: denied.detail,
155
- positiveControl,
156
- checkedAt,
157
- },
158
- });
159
- return {
160
- proceed: false,
161
- reason: `positive control ${targets.allowedHost} was unreachable (${allowed.detail}) — cannot distinguish egress enforcement from a network outage`,
162
- allowed,
163
- denied,
164
- };
165
- }
166
- // HEALTHY: allowed reachable AND denied blocked → the firewall is live.
167
- await sink.append({
168
- kind: 'egress_blocked',
169
- critical: true,
170
- payload: {
171
- host: targets.deniedHost,
172
- result: 'blocked',
173
- detail: denied.detail,
174
- positiveControl,
175
- checkedAt,
176
- },
177
- });
178
- return { proceed: true, reason: null, allowed, denied };
179
- }
180
- /**
181
- * Derive the known-ALLOWED positive-control host from the run's Orizu API base —
182
- * always allowlisted (it is the agent's control-plane path) and ours, so it is
183
- * the safest "must be reachable" signal. Falls back to a lenient host parse if
184
- * the base is not a well-formed URL.
185
- */
186
- export function egressCanaryAllowedHost(apiBaseUrl) {
187
- try {
188
- return new URL(apiBaseUrl).hostname;
189
- }
190
- catch {
191
- return apiBaseUrl.replace(/^[a-z]+:\/\//i, '').split('/')[0].split(':')[0];
192
- }
193
- }
45
+ import { buildOpenCodeSpawnEnv, defaultProbeEgress, egressCanaryAllowedHost, runEgressCanary, } from './hosted-loop-lifecycle.js';
46
+ // The lifecycle/orchestration seam moved to `hosted-loop-lifecycle.ts`
47
+ // (ALI-1015 Phase 1) so `lib/hosted-runtime/` can share it with a server-side
48
+ // coordinator. Re-exported here so this module's public surface is unchanged.
49
+ export { DEFAULT_EGRESS_CANARY_HOST, buildOpenCodeSpawnEnv, egressCanaryAllowedHost, runEgressCanary, } from './hosted-loop-lifecycle.js';
194
50
  /**
195
51
  * Default deferred-setup-hook runner (in-sandbox): run `bash .orizu/setup.sh` in
196
52
  * the workspace if present. Non-fatal — a missing hook or a non-zero exit is
@@ -225,9 +81,21 @@ function resolveIdleTimeoutMs(override) {
225
81
  }
226
82
  /** Model-key env vars whose value must be scrubbed from every run event. */
227
83
  const MODEL_KEY_ENV_VARS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY'];
84
+ /**
85
+ * Env var naming OTHER env vars whose values were injected as secrets — the
86
+ * server env-bundle pull (ALI-1055: `GET /api/coordinator/sessions/:id/
87
+ * env-bundle` returns `redactEnvVars`) writes it as a comma-separated list of
88
+ * names (e.g. `ANTHROPIC_API_KEY,BRAINTRUST_API_KEY`) so every pulled secret
89
+ * (model key + connector credentials) lands on the run-event redaction list.
90
+ */
91
+ export const INJECTED_ENV_VARS_ENV = 'ORIZU_INJECTED_ENV_VARS';
228
92
  function redactionListFromEnv() {
229
93
  const secrets = [];
230
- for (const key of MODEL_KEY_ENV_VARS) {
94
+ const injected = (process.env[INJECTED_ENV_VARS_ENV] ?? '')
95
+ .split(',')
96
+ .map(name => name.trim())
97
+ .filter(name => name.length > 0);
98
+ for (const key of [...MODEL_KEY_ENV_VARS, ...injected]) {
231
99
  const value = process.env[key];
232
100
  if (value)
233
101
  secrets.push(value);
@@ -175,9 +175,33 @@ async function requestToken(url, bearer, purpose, sessionId) {
175
175
  return { cred: null, status: res.status }
176
176
  }
177
177
 
178
- async function mint(ctx) {
178
+ // Resolve the Orizu bearer used to mint a repo token. PULL MODE (ALI-1057): when
179
+ // ctx.agentTokenUrl is set, exchange the durable BOOT SECRET (0600 file) for a
180
+ // FRESH short-lived bearer over HTTP on every mint — the DO-path credential
181
+ // identity is the boot secret, not a host-written 0600 bearer file. FILE MODE:
182
+ // read the 0600 bearer file exactly as before. The rest of mint() is unchanged.
183
+ async function resolveOrizuBearer(ctx) {
184
+ if (ctx.agentTokenUrl) {
185
+ let secret = ''
186
+ try { secret = fs.readFileSync(ctx.bootSecretFile, 'utf8').trim() } catch (_) { secret = '' }
187
+ if (!secret) throw new Error('empty boot secret file')
188
+ const res = await fetch(ctx.agentTokenUrl, {
189
+ method: 'GET',
190
+ headers: { Authorization: 'Bearer ' + secret },
191
+ })
192
+ if (!res.ok) throw new Error('agent-token pull failed (' + res.status + ')')
193
+ let data = null
194
+ try { data = await res.json() } catch (_) { data = null }
195
+ if (!data || !data.token) throw new Error('agent-token pull returned no token')
196
+ return String(data.token)
197
+ }
179
198
  const bearer = fs.readFileSync(ctx.bearerFile, 'utf8').trim()
180
199
  if (!bearer) throw new Error('empty bearer file')
200
+ return bearer
201
+ }
202
+
203
+ async function mint(ctx) {
204
+ const bearer = await resolveOrizuBearer(ctx)
181
205
  const base = String(ctx.apiBaseUrl || '').replace(/\\/$/, '')
182
206
  const url = base + '/api/cli/workspaces/' + encodeURIComponent(ctx.workspaceId) + '/repo-token'
183
207
  const purposes = ctx.tokenPurposes || {}
@@ -41,6 +41,7 @@ import { AGENT_GIT_IDENTITY, BEARER_BASENAME } from './hosted-runtime-assets.js'
41
41
  import { createVercelProvider } from './vercel-sandbox-provider.js';
42
42
  import { buildEgressPolicy, DEFAULT_EGRESS_CANARY_HOST } from './egress-policy.js';
43
43
  import { hostedLoopCommand } from './hosted-loop.js';
44
+ import { hostedBootCommand } from './hosted-boot.js';
44
45
  import { authedFetch } from './http.js';
45
46
  import { resolveBaseUrl } from './http.js';
46
47
  import { getWorkspaceRoot } from './workspace.js';
@@ -599,24 +600,102 @@ function numFlag(args, flag) {
599
600
  return Number.isFinite(parsed) ? parsed : undefined;
600
601
  }
601
602
  const HOSTED_USAGE = 'Usage: orizu session start --hosted --task "<prompt>" [--duration <min> (default 60, max 1440)] ' +
602
- '[--model <provider/model>] [--project <team/project>] [--provider vercel|local-sim] ' +
603
- '[--image <orizu-hosted-runtime:tag> (pre-baked VCR image; default env ORIZU_HOSTED_IMAGE)] ' +
604
- '[--snapshot <snapshot-id> (pre-baked Vercel snapshot, zero-Docker; default env ORIZU_HOSTED_SNAPSHOT; ' +
605
- 'mutually exclusive with --image)] [--model-key <key> (else ANTHROPIC_API_KEY; required)] [--tail]\n' +
606
- 'Note (v0): if you disconnect, the agent token expires within its TTL (<=60m) and the run finishes ' +
607
- 'cleanly; reconnect with `orizu run tail --run <id>`. The sandbox timeout is independent (up to 24h).';
603
+ '[--model <provider/model>] [--reasoning-effort <level>] [--project <team/project>] ' +
604
+ '[--title <title>] [--tail]\n' +
605
+ 'Default: the Orizu server provisions the sandbox (no VERCEL_* or model key needed on your machine); ' +
606
+ 'the session coordinator owns its lifetime (ALI-1055).\n' +
607
+ 'DEPRECATED escape hatch: --operator provisions from THIS machine (requires VERCEL_* + ANTHROPIC_API_KEY ' +
608
+ 'or --model-key; extra flags: [--provider vercel|local-sim] [--image <ref>] [--snapshot <id>]). ' +
609
+ 'Removal is planned after DO-coordinator validation (ALI-1055).\n' +
610
+ 'Note (v0, --operator only): if you disconnect, the agent token expires within its TTL (<=60m) and the run ' +
611
+ 'finishes cleanly; reconnect with `orizu run tail --run <id>`. The sandbox timeout is independent (up to 24h).';
612
+ /** Printed on EVERY `--operator` use (founder-locked: deprecate NOW, remove
613
+ * after Phase 6 validation). Loud on purpose. */
614
+ export const OPERATOR_DEPRECATION_WARNING = '! DEPRECATED: --operator (operator-provisioned hosted sessions) is deprecated; ' +
615
+ 'removal planned after DO-coordinator validation — ALI-1055. ' +
616
+ 'The default server path needs no VERCEL_* or model key on your machine.';
617
+ /**
618
+ * Default hosted path (ALI-1055 cutover): POST the server's hosted-sessions
619
+ * route and let the Durable Object coordinator own the sandbox lifetime. The
620
+ * CLI never reads VERCEL_* or a model key on this path — the server holds them.
621
+ */
622
+ export async function startHostedViaServer(args, workspaceId, task, tail, io, fetcher = (path, init) => authedFetch(path, init)) {
623
+ const payload = {
624
+ workspaceId,
625
+ task,
626
+ durationMinutes: numFlag(args, '--duration'),
627
+ model: argVal(args, '--model') ?? undefined,
628
+ reasoningEffort: argVal(args, '--reasoning-effort') ?? undefined,
629
+ projectSlug: argVal(args, '--project') ?? undefined,
630
+ title: argVal(args, '--title') ?? undefined,
631
+ };
632
+ let response;
633
+ try {
634
+ response = await fetcher('/api/cli/hosted-sessions', {
635
+ method: 'POST',
636
+ headers: { 'Content-Type': 'application/json' },
637
+ body: JSON.stringify(payload),
638
+ });
639
+ }
640
+ catch (error) {
641
+ io.printErr?.(`hosted session request failed: ${error instanceof Error ? error.message : String(error)}`);
642
+ return 1;
643
+ }
644
+ const body = (await response.json().catch(() => ({})));
645
+ const errorMessage = typeof body.error === 'string' ? body.error : `${response.status}`;
646
+ if (response.status === 503) {
647
+ io.printErr?.(errorMessage);
648
+ io.printErr?.('Re-run with `orizu session start --hosted --operator ...` to provision from your machine ' +
649
+ '(deprecated escape hatch; requires the Vercel token + snapshot on your side).');
650
+ return 1;
651
+ }
652
+ if (!response.ok) {
653
+ io.print(io.json ? JSON.stringify({ ok: false, error: errorMessage }) : `hosted session failed: ${errorMessage}`);
654
+ return 1;
655
+ }
656
+ const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';
657
+ const runId = typeof body.runId === 'string' ? body.runId : '';
658
+ if (io.json) {
659
+ io.print(JSON.stringify({ ok: true, sessionId, runId: runId || null, coordinator: body.coordinator ?? null }));
660
+ }
661
+ else {
662
+ io.print(`hosted session started: session ${sessionId}` +
663
+ (runId ? `, run ${runId}` : '') +
664
+ ' (coordinator-managed sandbox)');
665
+ if (!tail && runId)
666
+ io.print(`stream it: orizu run tail --run ${runId}`);
667
+ }
668
+ if (tail && runId) {
669
+ await tailWorkbenchRun({
670
+ fetcher,
671
+ runId,
672
+ onEvent: event => io.print(io.json
673
+ ? JSON.stringify(event)
674
+ : `#${String(event.sequence ?? '?')} ${String(event.eventType ?? '')}`),
675
+ });
676
+ }
677
+ return 0;
678
+ }
608
679
  /**
609
680
  * Dispatch `orizu internal hosted-loop` (in-sandbox) and `orizu session start
610
681
  * --hosted` (host-side). Kept out of index.ts so the CLI entrypoint stays a thin
611
682
  * dispatcher (ALI-976 ratchet).
683
+ *
684
+ * ALI-1055 cutover: the DEFAULT is the server path (POST /api/cli/hosted-sessions
685
+ * → DO coordinator). `--operator` keeps the old client-provisioned flow as a
686
+ * DEPRECATED escape hatch (loud warning on every use; removal after Phase 6).
612
687
  */
613
- export async function hostedCommand(args, io) {
688
+ export async function hostedCommand(args, io, deps = {}) {
614
689
  const positional = args.filter(arg => !arg.startsWith('--'));
615
690
  if (positional[0] === 'internal') {
616
691
  if (positional[1] === 'hosted-loop') {
617
692
  return hostedLoopCommand(args, io);
618
693
  }
619
- io.printErr?.('Usage: orizu internal hosted-loop --context <path>');
694
+ // ALI-1057: the DO-path in-sandbox boot entrypoint (ORIZU_SANDBOX_ENTRYPOINT).
695
+ if (positional[1] === 'hosted-boot') {
696
+ return hostedBootCommand(io);
697
+ }
698
+ io.printErr?.('Usage: orizu internal <hosted-loop --context <path> | hosted-boot>');
620
699
  return 1;
621
700
  }
622
701
  const task = argVal(args, '--task');
@@ -630,6 +709,16 @@ export async function hostedCommand(args, io) {
630
709
  return 1;
631
710
  }
632
711
  const tail = hasFlag(args, '--tail');
712
+ if (!hasFlag(args, '--operator')) {
713
+ // DEFAULT (ALI-1055): server-provisioned. No provider, no VERCEL_* read, no
714
+ // model key on the customer machine — the coordinator owns the lifetime.
715
+ return (deps.startViaServer ?? startHostedViaServer)(args, workspaceId, task, tail, io);
716
+ }
717
+ // DEPRECATED operator escape hatch — warn LOUDLY on every use.
718
+ io.printErr?.(OPERATOR_DEPRECATION_WARNING);
719
+ if (deps.startViaOperator) {
720
+ return deps.startViaOperator(args, workspaceId, task, tail, io);
721
+ }
633
722
  const providerKind = (argVal(args, '--provider') ?? 'vercel').toLowerCase();
634
723
  const provider = providerKind === 'local-sim' ? createLocalSimProvider() : createVercelProvider();
635
724
  const hostFetcher = (path, init) => authedFetch(path, init);
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ import { connectorsCommand } from './connectors-cli.js';
30
30
  import { ARTIFACT_MAX_BYTES, runnerOptimizerCommand } from './artifact-pull.js';
31
31
  import { killSwitchCommand } from './kill-switch-cli.js';
32
32
  import { egressAllowlistCommand } from './egress-allowlist-cli.js';
33
+ import { teamConnectorsCommand } from './team-connectors-cli.js';
33
34
  import { manifestsCommand } from './manifests-cli.js';
34
35
  import { workbenchCommand } from './workbench-cli.js';
35
36
  import { hostedCommand } from './hosted-session-cli.js';
@@ -5168,6 +5169,23 @@ export async function main(rawArgs = process.argv.slice(2)) {
5168
5169
  process.exitCode = await egressAllowlistCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine, printErr: printError });
5169
5170
  return;
5170
5171
  }
5172
+ if (command === 'team' && subcommand === 'connectors') {
5173
+ process.exitCode = await teamConnectorsCommand(cliArgs.slice(2), {
5174
+ json: hasJsonFlag(),
5175
+ print: printLine,
5176
+ printErr: printError,
5177
+ resolveTeamSlug: async () => {
5178
+ const noInput = hasArg('--no-input') || hasArg('--non-interactive') || !isInteractiveTerminal();
5179
+ try {
5180
+ return (await resolveSetupTeam(getOptionalArgValue('--team'), noInput, false)).slug;
5181
+ }
5182
+ catch {
5183
+ return null;
5184
+ }
5185
+ },
5186
+ });
5187
+ return;
5188
+ }
5171
5189
  printUsage();
5172
5190
  process.exit(1);
5173
5191
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Provider-neutral sandbox token-hygiene sweep (ALI-973 / ALI-1050).
3
+ *
4
+ * Extracted out of `daytona-slice.ts` so the hygiene helper carries NO Daytona
5
+ * coupling: it works against any `SandboxProvider`/`SandboxSession` (ADR-003
6
+ * seam) and is imported by the shipped hosted-bootstrap path (`hosted-bootstrap
7
+ * .ts`) as well as the founder-only Daytona slice. Keeping it here means the
8
+ * shipped code never transitively reaches the optional `@daytonaio/sdk`.
9
+ */
10
+ /**
11
+ * The list of places a short-lived git token can leak inside a sandbox after a
12
+ * transient clone+push. List-driven so it is trivial to extend. `${repo}` is
13
+ * substituted with the repo path. Ordered from the repo-local stores outward to
14
+ * global config, the credential cache, the reflog, shell history, and the live
15
+ * process listing / argv — the exact surfaces the Daytona-support question names.
16
+ */
17
+ export function tokenLeakProbes(repoPath) {
18
+ const R = repoPath;
19
+ return [
20
+ { location: `${R}/.git/config`, command: `cat ${R}/.git/config 2>/dev/null || true` },
21
+ { location: `${R}/.git/logs (reflog)`, command: `find ${R}/.git/logs -type f -exec cat {} + 2>/dev/null || true` },
22
+ { location: '~/.git-credentials', command: 'cat "$HOME/.git-credentials" 2>/dev/null || true' },
23
+ { location: '~/.gitconfig (global)', command: 'cat "$HOME/.gitconfig" 2>/dev/null || true' },
24
+ { location: 'git credential cache/store', command: 'cat "$HOME/.cache/git/credential/"* "${XDG_CACHE_HOME:-$HOME/.cache}/git/credential/"* 2>/dev/null || true' },
25
+ { location: 'shell history', command: 'cat "$HOME/.bash_history" "$HOME/.zsh_history" 2>/dev/null || true' },
26
+ { location: 'process listing / argv', command: 'ps -eo args 2>/dev/null || ps aux 2>/dev/null || true' },
27
+ { location: 'process env', command: 'env' },
28
+ ];
29
+ }
30
+ /**
31
+ * Grep the sandbox filesystem + process table for token values and the
32
+ * `x-access-token` marker across every known leak surface (see `tokenLeakProbes`).
33
+ * Returns every finding; an empty array means the transient operations left no
34
+ * residue. This is the load-bearing check the whole slice exists to make.
35
+ *
36
+ * NOTE (local-sim): the reflog/config/credential-store/history probes run
37
+ * meaningfully in local-sim, but `ps`/argv coverage is only decisive on the live
38
+ * Daytona run (local-sim runs the git clone via a child process on the host, not
39
+ * inside an isolated PID namespace). The WS-F memo marks that probe live-only.
40
+ */
41
+ export async function sweepForTokenResidue(session, opts) {
42
+ const markers = opts.markers ?? ['x-access-token'];
43
+ const tokens = opts.tokens.filter(token => token.length > 0);
44
+ const probes = opts.probes ?? tokenLeakProbes(opts.repoPath);
45
+ const findings = [];
46
+ for (const probe of probes) {
47
+ const { stdout } = await session.exec(probe.command);
48
+ for (const token of tokens) {
49
+ if (stdout.includes(token)) {
50
+ findings.push({ location: probe.location, marker: 'token-value' });
51
+ }
52
+ }
53
+ for (const marker of markers) {
54
+ if (stdout.includes(marker)) {
55
+ findings.push({ location: probe.location, marker });
56
+ }
57
+ }
58
+ }
59
+ return findings;
60
+ }
@@ -51,7 +51,13 @@ import { basename, dirname, join } from 'path';
51
51
  // cost is a one-time developer/CI tax on this package, not a runtime or
52
52
  // shipped-artifact cost. Re-evaluate if @daytonaio/sdk ever becomes a
53
53
  // dependency of a package that IS shipped to end users.
54
- const DAYTONA_SDK_SPECIFIER = '@daytonaio/sdk';
54
+ //
55
+ // The specifier is ASSEMBLED at runtime (join, not a literal) so a bundler that
56
+ // reaches this module through the shared `lib/hosted-runtime/` surface (ALI-1015:
57
+ // `next build` for the app, wrangler for the coordinator Worker) cannot
58
+ // constant-fold `import(specifier)` into a static dependency on the SDK — the
59
+ // same discipline as the ALI-1031 server provider.
60
+ const DAYTONA_SDK_SPECIFIER = ['@daytonaio', 'sdk'].join('/');
55
61
  async function loadDaytonaModule() {
56
62
  try {
57
63
  const specifier = DAYTONA_SDK_SPECIFIER;