orizu 0.5.49 → 0.5.50
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/dist/account-directory.d.ts +2 -2
- package/dist/account-directory.d.ts.map +1 -1
- package/dist/account-directory.js +7 -32
- package/dist/auth-commands.d.ts.map +1 -1
- package/dist/auth-commands.js +22 -30
- package/dist/credentials.d.ts +5 -0
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +84 -3
- package/dist/dataset-transfer-upload.d.ts.map +1 -1
- package/dist/dataset-transfer-upload.js +6 -1
- package/dist/directory-list-read.d.ts +37 -0
- package/dist/directory-list-read.d.ts.map +1 -0
- package/dist/directory-list-read.js +87 -0
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +59 -41
- package/dist/hosted-harness-opencode.d.ts +31 -0
- package/dist/hosted-harness-opencode.d.ts.map +1 -1
- package/dist/hosted-harness-opencode.js +45 -7
- package/dist/hosted-loop.d.ts +39 -2
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +127 -18
- package/dist/hosted-session-seed.d.ts +2 -0
- package/dist/hosted-session-seed.d.ts.map +1 -1
- package/dist/hosted-session-seed.js +27 -10
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +39 -234
- package/dist/keyboard-select.d.ts.map +1 -1
- package/dist/keyboard-select.js +23 -45
- package/dist/project-create-workflow.d.ts +18 -0
- package/dist/project-create-workflow.d.ts.map +1 -0
- package/dist/project-create-workflow.js +121 -0
- package/dist/self-update-command.d.ts +8 -0
- package/dist/self-update-command.d.ts.map +1 -0
- package/dist/self-update-command.js +390 -0
- package/dist/self-update.d.ts +29 -16
- package/dist/self-update.d.ts.map +1 -1
- package/dist/self-update.js +55 -244
- package/dist/setup-account.d.ts.map +1 -1
- package/dist/setup-account.js +4 -1
- package/dist/setup-and-skills-commands.d.ts.map +1 -1
- package/dist/setup-and-skills-commands.js +88 -102
- package/dist/skill-install-workflow.d.ts +23 -0
- package/dist/skill-install-workflow.d.ts.map +1 -0
- package/dist/skill-install-workflow.js +184 -0
- package/dist/skill-installer.d.ts +8 -3
- package/dist/skill-installer.d.ts.map +1 -1
- package/dist/skill-installer.js +26 -33
- package/dist/stored-session-requests.d.ts +27 -0
- package/dist/stored-session-requests.d.ts.map +1 -0
- package/dist/stored-session-requests.js +264 -0
- package/dist/terminal-key-session.d.ts +23 -0
- package/dist/terminal-key-session.d.ts.map +1 -0
- package/dist/terminal-key-session.js +74 -0
- package/package.json +1 -1
- package/src/account-directory.ts +8 -42
- package/src/auth-commands.ts +20 -29
- package/src/credentials.ts +83 -3
- package/src/dataset-transfer-upload.ts +7 -1
- package/src/directory-list-read.ts +123 -0
- package/src/hosted-boot.ts +65 -43
- package/src/hosted-harness-opencode.ts +65 -9
- package/src/hosted-loop.ts +152 -18
- package/src/hosted-session-seed.ts +25 -8
- package/src/http.ts +39 -317
- package/src/keyboard-select.ts +18 -55
- package/src/project-create-workflow.ts +139 -0
- package/src/self-update-command.ts +419 -0
- package/src/self-update.ts +70 -266
- package/src/setup-account.ts +4 -1
- package/src/setup-and-skills-commands.ts +89 -121
- package/src/skill-install-workflow.ts +215 -0
- package/src/skill-installer.ts +31 -38
- package/src/stored-session-requests.ts +316 -0
- package/src/terminal-key-session.ts +88 -0
package/src/hosted-loop.ts
CHANGED
|
@@ -53,13 +53,17 @@ import { annotateHeadlessQuestions, withIdleWatchdog } from './hosted-headless.j
|
|
|
53
53
|
import {
|
|
54
54
|
DEFAULT_PROMPT_MAX_DURATION_MS,
|
|
55
55
|
OPENCODE_PINNED_VERSION,
|
|
56
|
+
OpenCodeExitedError,
|
|
56
57
|
awaitOpenCodeModelResolvable,
|
|
57
58
|
createOpenCodeHarness,
|
|
58
59
|
permissionForOrigin,
|
|
59
60
|
spawnOpenCode,
|
|
61
|
+
startOpenCodeProcess,
|
|
62
|
+
waitOpenCodeReady,
|
|
60
63
|
type ModelValidationOutcome,
|
|
61
64
|
type SpawnOpenCodeOptions,
|
|
62
65
|
type SpawnedOpenCode,
|
|
66
|
+
type StartedOpenCode,
|
|
63
67
|
} from './hosted-harness-opencode.js'
|
|
64
68
|
import { createClaudeAgentHarness } from './hosted-harness-claude.js'
|
|
65
69
|
import { INJECTED_ENV_VARS_ENV } from './hosted-environment.js'
|
|
@@ -137,6 +141,25 @@ interface HostedLoopSharedRuntime {
|
|
|
137
141
|
startupComplete: boolean
|
|
138
142
|
spawnLogPath: string
|
|
139
143
|
closed: boolean
|
|
144
|
+
/** ORI-2512: the boot's background start, until the first turn takes it. */
|
|
145
|
+
prestart?: HostedPrestart | null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* ORI-2512 cut 3: the egress canary's probes and the OpenCode start, begun by
|
|
150
|
+
* the boot while the repository clones. The first turn records the canary's
|
|
151
|
+
* events from the stored answers (same decision, same events) and waits for
|
|
152
|
+
* the started server, which is asked nothing before then.
|
|
153
|
+
*/
|
|
154
|
+
export interface HostedPrestart {
|
|
155
|
+
/** The probes' answers by host, and the canary's verdict. Null when a
|
|
156
|
+
* probe threw: the turn then runs the canary itself. Never rejects. */
|
|
157
|
+
canary: Promise<{ results: Map<string, EgressProbeResult>; checkedAt: number; proceed: boolean } | null>
|
|
158
|
+
/** The started process; null when the canary said no or the start was
|
|
159
|
+
* stopped. Rejects when the background start failed. */
|
|
160
|
+
opencode: Promise<StartedOpenCode | null>
|
|
161
|
+
/** Stop the process, now or as soon as it starts. */
|
|
162
|
+
stop: () => void
|
|
140
163
|
}
|
|
141
164
|
|
|
142
165
|
export interface HostedLoopSessionContext extends Omit<HostedLoopContext, 'runId' | 'bearerFile' | 'taskFile' | 'messageId'> {
|
|
@@ -362,7 +385,7 @@ export function installOpenCodePinned(version: string, env?: NodeJS.ProcessEnv):
|
|
|
362
385
|
function nodeChildSpawner(
|
|
363
386
|
cmd: string[],
|
|
364
387
|
opts: { cwd?: string; env: Record<string, string>; logPath?: string }
|
|
365
|
-
): { kill: () => void } {
|
|
388
|
+
): { kill: () => void; hasExited: () => boolean; exitReason: () => string | null } {
|
|
366
389
|
let stdio: 'ignore' | Array<'ignore' | number> = 'ignore'
|
|
367
390
|
let fd: number | null = null
|
|
368
391
|
if (opts.logPath) {
|
|
@@ -386,7 +409,81 @@ function nodeChildSpawner(
|
|
|
386
409
|
// already closed
|
|
387
410
|
}
|
|
388
411
|
}
|
|
389
|
-
|
|
412
|
+
// ORI-2512: lets the readiness wait stop at once for a process that has
|
|
413
|
+
// gone. The error listener also keeps a failed spawn from crashing the boot.
|
|
414
|
+
let exited = false
|
|
415
|
+
let spawnError: string | null = null
|
|
416
|
+
child.once('exit', () => { exited = true })
|
|
417
|
+
child.once('error', error => {
|
|
418
|
+
exited = true
|
|
419
|
+
spawnError = error instanceof Error ? error.message : String(error)
|
|
420
|
+
})
|
|
421
|
+
return { kill: () => child.kill(), hasExited: () => exited, exitReason: () => spawnError }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** The one place the hosted `opencode serve` options come from, for both the
|
|
425
|
+
* boot's background start and the turn's start in order. */
|
|
426
|
+
function openCodeSpawnOptions(context: HostedLoopContext, logPath: string): SpawnOpenCodeOptions {
|
|
427
|
+
return {
|
|
428
|
+
model: context.model,
|
|
429
|
+
permission: permissionForOrigin(context.sessionOrigin),
|
|
430
|
+
projectId: context.projectId,
|
|
431
|
+
cwd: context.workspaceDir,
|
|
432
|
+
port: context.opencodePort,
|
|
433
|
+
env: buildOpenCodeSpawnEnv(context),
|
|
434
|
+
spawn: nodeChildSpawner,
|
|
435
|
+
logPath,
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* ORI-2512 cut 3: start the egress canary and then OpenCode in the
|
|
441
|
+
* background, for the boot to call before it clones. The order is the
|
|
442
|
+
* loop's: the canary first, fail-closed, before the models.dev refresh or
|
|
443
|
+
* OpenCode. Nothing here records events; the first turn does. Only for the
|
|
444
|
+
* pre-baked OpenCode runtime under an enforced egress policy; otherwise null
|
|
445
|
+
* and the turn starts everything in order.
|
|
446
|
+
*/
|
|
447
|
+
export function prestartHostedOpenCode(
|
|
448
|
+
context: HostedLoopContext,
|
|
449
|
+
opts: { logPath: string }
|
|
450
|
+
): HostedPrestart | null {
|
|
451
|
+
if ((context.harness ?? 'opencode') !== 'opencode' || !context.egressCanaryHost || context.prebaked !== true) return null
|
|
452
|
+
const targets: EgressCanaryTargets = {
|
|
453
|
+
allowedHost: egressCanaryAllowedHost(context.apiBaseUrl),
|
|
454
|
+
deniedHost: context.egressCanaryHost,
|
|
455
|
+
}
|
|
456
|
+
const results = new Map<string, EgressProbeResult>()
|
|
457
|
+
let stopped = false
|
|
458
|
+
const canary = (async () => {
|
|
459
|
+
let checkedAt = Date.now()
|
|
460
|
+
// The loop's own decision, with nothing recorded yet.
|
|
461
|
+
const decision = await runEgressCanary(targets, { append: async () => {} }, async host => {
|
|
462
|
+
const result = await defaultProbeEgress(host)
|
|
463
|
+
results.set(host, result)
|
|
464
|
+
return result
|
|
465
|
+
}, () => (checkedAt = Date.now()))
|
|
466
|
+
return { results, checkedAt, proceed: decision.proceed }
|
|
467
|
+
})().catch(() => null)
|
|
468
|
+
const opencode = canary.then(async stored => {
|
|
469
|
+
if (!stored?.proceed || stopped) return null
|
|
470
|
+
const started = await startOpenCodeProcess(openCodeSpawnOptions(context, opts.logPath))
|
|
471
|
+
if (stopped) {
|
|
472
|
+
started.stop()
|
|
473
|
+
return null
|
|
474
|
+
}
|
|
475
|
+
return started
|
|
476
|
+
})
|
|
477
|
+
// The turn reads a failure; until then it must not count as unhandled.
|
|
478
|
+
opencode.catch(() => {})
|
|
479
|
+
return {
|
|
480
|
+
canary,
|
|
481
|
+
opencode,
|
|
482
|
+
stop: () => {
|
|
483
|
+
stopped = true
|
|
484
|
+
void opencode.then(started => started?.stop(), () => {})
|
|
485
|
+
},
|
|
486
|
+
}
|
|
390
487
|
}
|
|
391
488
|
|
|
392
489
|
/** ORI-2512: the longest the end-of-turn save waits for the boot's
|
|
@@ -449,15 +546,22 @@ export async function runHostedLoop(opts: RunHostedLoopOptions): Promise<HostedL
|
|
|
449
546
|
// unreachable → network broken / indistinguishable from a block) emits
|
|
450
547
|
// `egress_allowed` and finishes the run FAILED rather than touch real customer
|
|
451
548
|
// data. A canary whose own event append fails is likewise fail-closed.
|
|
549
|
+
// ORI-2512: on the first turn, the boot may already have probed in the
|
|
550
|
+
// background; the same decision is then made from its stored answers.
|
|
551
|
+
const prestart = opts.sharedRuntime && !opts.sharedRuntime.startupComplete ? opts.sharedRuntime.prestart : null
|
|
452
552
|
if (context.egressCanaryHost && !opts.sharedRuntime?.startupComplete) {
|
|
453
|
-
const
|
|
553
|
+
const liveProbe = opts.probeEgress ?? defaultProbeEgress
|
|
554
|
+
const stored = prestart ? await prestart.canary : null
|
|
555
|
+
const probe = stored
|
|
556
|
+
? async (host: string): Promise<EgressProbeResult> => stored.results.get(host) ?? liveProbe(host)
|
|
557
|
+
: liveProbe
|
|
454
558
|
const targets: EgressCanaryTargets = {
|
|
455
559
|
allowedHost: egressCanaryAllowedHost(context.apiBaseUrl),
|
|
456
560
|
deniedHost: context.egressCanaryHost,
|
|
457
561
|
}
|
|
458
562
|
let canary: EgressCanaryDecision
|
|
459
563
|
try {
|
|
460
|
-
canary = await runEgressCanary(targets, sink, probe, opts.now)
|
|
564
|
+
canary = await runEgressCanary(targets, sink, probe, stored ? () => stored.checkedAt : opts.now)
|
|
461
565
|
} catch (error) {
|
|
462
566
|
// The canary's own event append failed — treat as fail-closed: we cannot
|
|
463
567
|
// prove the firewall is live, so do not proceed.
|
|
@@ -666,26 +770,49 @@ export async function runHostedLoop(opts: RunHostedLoopOptions): Promise<HostedL
|
|
|
666
770
|
})
|
|
667
771
|
}
|
|
668
772
|
|
|
773
|
+
const logPath = opts.sharedRuntime?.spawnLogPath ?? join(tmpdir(), `opencode-serve-${context.runId}.log`)
|
|
774
|
+
// ORI-2512: the first turn takes the server the boot started during the
|
|
775
|
+
// clone and asks it something only now, after the clone. A background
|
|
776
|
+
// start that failed, or a process that already exited, is started again
|
|
777
|
+
// here in order.
|
|
778
|
+
const waitStartedAt = Date.now()
|
|
779
|
+
let prestarted: SpawnedOpenCode | null = null
|
|
780
|
+
if (prestart && opts.sharedRuntime) {
|
|
781
|
+
opts.sharedRuntime.prestart = null
|
|
782
|
+
let started: StartedOpenCode | null = null
|
|
783
|
+
try {
|
|
784
|
+
started = await prestart.opencode
|
|
785
|
+
} catch {
|
|
786
|
+
diagnose('hosted OpenCode background start failed; starting it in order')
|
|
787
|
+
}
|
|
788
|
+
if (started) {
|
|
789
|
+
try {
|
|
790
|
+
prestarted = await waitOpenCodeReady(started, { logPath, signal })
|
|
791
|
+
} catch (error) {
|
|
792
|
+
if (!(error instanceof OpenCodeExitedError)) throw error
|
|
793
|
+
diagnose('hosted OpenCode background start exited; starting it in order')
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
669
797
|
const spawnImpl = opts.spawnOpenCode ?? spawnOpenCode
|
|
670
798
|
// The default spawnOpenCode WAITS for the server to answer HTTP before
|
|
671
799
|
// returning — OpenCode's first boot runs a one-time sqlite migration, and
|
|
672
800
|
// starting the harness before the server listens raced straight into a
|
|
673
801
|
// connection-refused "fetch failed" (ALI-1034).
|
|
674
|
-
spawned = await spawnImpl({
|
|
675
|
-
model: context.model,
|
|
676
|
-
permission: permissionForOrigin(context.sessionOrigin),
|
|
677
|
-
projectId: context.projectId,
|
|
678
|
-
cwd: context.workspaceDir,
|
|
679
|
-
port: context.opencodePort,
|
|
680
|
-
env: buildOpenCodeSpawnEnv(context),
|
|
681
|
-
spawn: nodeChildSpawner,
|
|
682
|
-
logPath: opts.sharedRuntime?.spawnLogPath ?? join(tmpdir(), `opencode-serve-${context.runId}.log`),
|
|
683
|
-
signal,
|
|
684
|
-
})
|
|
802
|
+
spawned = prestarted ?? await spawnImpl({ ...openCodeSpawnOptions(context, logPath), signal })
|
|
685
803
|
if (typeof spawned.readyAfterMs === 'number') {
|
|
686
804
|
await sink.append({
|
|
687
805
|
kind: 'artifact',
|
|
688
|
-
payload: {
|
|
806
|
+
payload: {
|
|
807
|
+
step: 'opencode_ready',
|
|
808
|
+
ok: true,
|
|
809
|
+
// How long this turn waited for OpenCode. When the boot started it
|
|
810
|
+
// during the clone, that is the wait after the clone; the time
|
|
811
|
+
// since it was started (clone included) is kept apart.
|
|
812
|
+
...(prestarted
|
|
813
|
+
? { waitedMs: Date.now() - waitStartedAt, prestarted: true, sinceStartMs: spawned.readyAfterMs }
|
|
814
|
+
: { waitedMs: spawned.readyAfterMs }),
|
|
815
|
+
},
|
|
689
816
|
})
|
|
690
817
|
}
|
|
691
818
|
|
|
@@ -979,7 +1106,8 @@ export async function runHostedLoop(opts: RunHostedLoopOptions): Promise<HostedL
|
|
|
979
1106
|
}
|
|
980
1107
|
|
|
981
1108
|
export async function startHostedLoopSession(
|
|
982
|
-
context: HostedLoopSessionContext
|
|
1109
|
+
context: HostedLoopSessionContext,
|
|
1110
|
+
options: { prestart?: HostedPrestart | null } = {}
|
|
983
1111
|
): Promise<HostedLoopSessionHandle> {
|
|
984
1112
|
return {
|
|
985
1113
|
context,
|
|
@@ -989,12 +1117,18 @@ export async function startHostedLoopSession(
|
|
|
989
1117
|
agentSessionId: null,
|
|
990
1118
|
installOk: false,
|
|
991
1119
|
startupComplete: false,
|
|
992
|
-
spawnLogPath:
|
|
1120
|
+
spawnLogPath: hostedSessionOpenCodeLogPath(context),
|
|
993
1121
|
closed: false,
|
|
1122
|
+
prestart: options.prestart ?? null,
|
|
994
1123
|
},
|
|
995
1124
|
}
|
|
996
1125
|
}
|
|
997
1126
|
|
|
1127
|
+
/** Where a hosted session's `opencode serve` writes its output. */
|
|
1128
|
+
export function hostedSessionOpenCodeLogPath(context: Pick<HostedLoopSessionContext, 'sessionDir' | 'sessionId'>): string {
|
|
1129
|
+
return join(context.sessionDir, `opencode-serve-${context.sessionId}.log`)
|
|
1130
|
+
}
|
|
1131
|
+
|
|
998
1132
|
export async function runHostedLoopTurn(
|
|
999
1133
|
session: HostedLoopSessionHandle,
|
|
1000
1134
|
turn: HostedLoopTurnContext,
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* boot's credential helper), never in a URL or argument.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { existsSync, readdirSync, renameSync } from 'fs'
|
|
21
|
+
import { join } from 'path'
|
|
22
|
+
|
|
20
23
|
export const SESSION_COPY_SEED_FAILURE = 'session copy could not be filled from the team copy'
|
|
21
24
|
|
|
22
25
|
interface SeedExecResult {
|
|
@@ -109,6 +112,8 @@ export interface CloneTeamIntoWorkTreeInput {
|
|
|
109
112
|
scratchDir: string
|
|
110
113
|
/** The boot's working folder: absent or empty. */
|
|
111
114
|
workTree: string
|
|
115
|
+
/** Moves one entry into the working folder (default: renameSync). */
|
|
116
|
+
rename?: (from: string, to: string) => void
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
export type CloneTeamIntoWorkTreeResult =
|
|
@@ -156,14 +161,26 @@ export async function cloneTeamIntoWorkTree(input: CloneTeamIntoWorkTreeInput):
|
|
|
156
161
|
if (fetched.status !== 0) throw failure('reading the team copy', fetched)
|
|
157
162
|
const pointed = input.exec('git', ['-C', input.scratchDir, 'remote', 'set-url', 'origin', input.sessionRemote])
|
|
158
163
|
if (pointed.status !== 0) throw failure('pointing the working folder at the session copy', pointed)
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
// ORI-2512: an existing folder (the runtime image may ship it empty, and
|
|
165
|
+
// the boot may have started OpenCode in it) is filled in place, never
|
|
166
|
+
// replaced: a process already started there would be left in a deleted
|
|
167
|
+
// folder. A missing folder becomes the clone.
|
|
168
|
+
if (existsSync(input.workTree)) {
|
|
169
|
+
if (readdirSync(input.workTree).length > 0) throw failure('making the working folder', { status: 1, stdout: '', stderr: `${input.workTree} is not empty` })
|
|
170
|
+
try {
|
|
171
|
+
// `.git` last: a fill that stops partway leaves no repository, which
|
|
172
|
+
// the next boot refuses, rather than one missing files that a save
|
|
173
|
+
// would push as deleted.
|
|
174
|
+
const entries = readdirSync(input.scratchDir).sort((a, b) => Number(a === '.git') - Number(b === '.git'))
|
|
175
|
+
const rename = input.rename ?? renameSync
|
|
176
|
+
for (const entry of entries) rename(join(input.scratchDir, entry), join(input.workTree, entry))
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw failure('making the working folder', { status: 1, stdout: '', stderr: error instanceof Error ? error.message : String(error) })
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
const moved = input.exec('mv', ['--', input.scratchDir, input.workTree])
|
|
182
|
+
if (moved.status !== 0) throw failure('making the working folder', moved)
|
|
183
|
+
}
|
|
167
184
|
} finally {
|
|
168
185
|
clear()
|
|
169
186
|
}
|