orizu 0.5.41 → 0.5.42
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.map +1 -1
- package/dist/account-directory.js +2 -10
- package/dist/app-publish-workspace.js +1 -1
- package/dist/archive-artifact-workflow.d.ts +23 -0
- package/dist/archive-artifact-workflow.d.ts.map +1 -0
- package/dist/archive-artifact-workflow.js +100 -0
- package/dist/archive-artifacts-cli.d.ts.map +1 -1
- package/dist/archive-artifacts-cli.js +7 -11
- package/dist/dataset-commands.d.ts.map +1 -1
- package/dist/dataset-commands.js +19 -29
- package/dist/dataset-split-create-workflow.d.ts +16 -0
- package/dist/dataset-split-create-workflow.d.ts.map +1 -0
- package/dist/dataset-split-create-workflow.js +115 -0
- package/dist/git-credential.js +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +10 -2
- package/dist/hosted-boot.d.ts +7 -0
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +251 -58
- package/dist/hosted-harness.d.ts +1 -0
- package/dist/hosted-harness.d.ts.map +1 -1
- package/dist/hosted-harness.js +2 -0
- package/dist/hosted-harvest.d.ts +2 -1
- package/dist/hosted-harvest.d.ts.map +1 -1
- package/dist/hosted-harvest.js +29 -17
- package/dist/hosted-live-channel.d.ts +25 -0
- package/dist/hosted-live-channel.d.ts.map +1 -0
- package/dist/hosted-live-channel.js +130 -0
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +18 -0
- package/dist/hosted-run-event-sink.d.ts +3 -0
- package/dist/hosted-run-event-sink.d.ts.map +1 -1
- package/dist/hosted-run-event-sink.js +4 -0
- package/dist/hosted-runtime-assets.js +1 -1
- package/dist/hosted-session-cli.d.ts.map +1 -1
- package/dist/hosted-session-cli.js +4 -0
- package/dist/hosted-session-seed.d.ts +8 -6
- package/dist/hosted-session-seed.d.ts.map +1 -1
- package/dist/hosted-session-seed.js +26 -9
- package/dist/hosted-skill-staging.d.ts +36 -12
- package/dist/hosted-skill-staging.d.ts.map +1 -1
- package/dist/hosted-skill-staging.js +96 -34
- package/dist/http.d.ts +2 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +10 -6
- package/dist/orizu-skill-links.d.ts +48 -0
- package/dist/orizu-skill-links.d.ts.map +1 -0
- package/dist/orizu-skill-links.js +99 -0
- package/dist/preview-runtime.d.ts +1 -0
- package/dist/preview-runtime.d.ts.map +1 -1
- package/dist/preview-runtime.js +15 -0
- package/dist/setup-and-skills-commands.d.ts.map +1 -1
- package/dist/setup-and-skills-commands.js +52 -7
- package/dist/setup-onboarding.d.ts.map +1 -1
- package/dist/setup-onboarding.js +6 -2
- package/dist/skill-installer.d.ts +16 -0
- package/dist/skill-installer.d.ts.map +1 -1
- package/dist/skill-installer.js +33 -5
- package/dist/team-create-workflow.d.ts +16 -0
- package/dist/team-create-workflow.d.ts.map +1 -0
- package/dist/team-create-workflow.js +130 -0
- package/dist/workbench-cli.d.ts.map +1 -1
- package/dist/workbench-cli.js +24 -4
- package/package.json +1 -1
- package/src/account-directory.ts +2 -10
- package/src/app-publish-workspace.ts +1 -1
- package/src/archive-artifact-workflow.ts +101 -0
- package/src/archive-artifacts-cli.ts +7 -28
- package/src/dataset-commands.ts +19 -31
- package/src/dataset-split-create-workflow.ts +128 -0
- package/src/git-credential.ts +1 -1
- package/src/help.ts +10 -2
- package/src/hosted-boot.ts +252 -57
- package/src/hosted-harness.ts +2 -0
- package/src/hosted-harvest.ts +30 -17
- package/src/hosted-live-channel.ts +136 -0
- package/src/hosted-loop.ts +16 -0
- package/src/hosted-run-event-sink.ts +7 -0
- package/src/hosted-runtime-assets.ts +1 -1
- package/src/hosted-session-cli.ts +4 -0
- package/src/hosted-session-seed.ts +28 -10
- package/src/hosted-skill-staging.ts +110 -36
- package/src/http.ts +12 -7
- package/src/orizu-skill-links.ts +114 -0
- package/src/preview-runtime.ts +16 -0
- package/src/setup-and-skills-commands.ts +60 -6
- package/src/setup-onboarding.ts +6 -2
- package/src/skill-installer.ts +44 -5
- package/src/team-create-workflow.ts +132 -0
- package/src/workbench-cli.ts +24 -4
package/dist/hosted-boot.js
CHANGED
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
* non-secret ANTHROPIC_API_KEY dummy placeholder (firewall brokers the real key).
|
|
31
31
|
*/
|
|
32
32
|
import { spawnSync } from 'child_process';
|
|
33
|
-
import {
|
|
33
|
+
import { randomUUID } from 'crypto';
|
|
34
|
+
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync } from 'fs';
|
|
34
35
|
import { isValidCloudflareArtifactsGitRemote } from './cloudflare-artifacts-git-remote.js';
|
|
35
36
|
import { seedSessionCopy } from './hosted-session-seed.js';
|
|
36
37
|
import { AGENT_GIT_IDENTITY, BEARER_BASENAME, BOOT_CONTEXT_BASENAME, DEFAULT_CACHE_REFRESH_BUFFER_MS, HELPER_SCRIPT_BASENAME, REPO_CRED_CACHE_BASENAME, renderCredentialHelperScript, serializeBootContext, } from './hosted-runtime-assets.js';
|
|
@@ -39,6 +40,7 @@ import { DEFAULT_EGRESS_CANARY_HOST, DEFAULT_HOSTED_MODEL } from './hosted-loop-
|
|
|
39
40
|
import { composeHostedAnswerPrompt } from './hosted-question.js';
|
|
40
41
|
import { resumeRunEventSink } from './hosted-run-event-sink.js';
|
|
41
42
|
import { stageOrizuSkill } from './hosted-skill-staging.js';
|
|
43
|
+
import { openHostedLiveChannel } from './hosted-live-channel.js';
|
|
42
44
|
/** Non-secret placeholder model key (firewall brokers the real org key on
|
|
43
45
|
* egress). Byte-for-byte the operator path's constant
|
|
44
46
|
* (packages/cli/src/hosted-session-cli.ts `ANTHROPIC_DUMMY_KEY` and the
|
|
@@ -314,7 +316,7 @@ function asPositiveNumber(value) {
|
|
|
314
316
|
/** GET the session (agent-capable, RLS-scoped) to learn its workspace, branch,
|
|
315
317
|
* task, and any existing run — the DO start body carries none of these. */
|
|
316
318
|
export async function resolveSession(opts) {
|
|
317
|
-
const body = await bearerJson(opts.fetchImpl, `${opts.baseUrl}/api/cli/sessions/${encodeURIComponent(opts.sessionId)}`, opts.bearer, opts.signal ? { signal: opts.signal } : {});
|
|
319
|
+
const body = await bearerJson(opts.fetchImpl, `${opts.baseUrl}/api/cli/sessions/${encodeURIComponent(opts.sessionId)}`, opts.bearer, { headers: { 'X-Orizu-Session-Seed-Protocol': '1' }, ...(opts.signal ? { signal: opts.signal } : {}) });
|
|
318
320
|
const session = (body.session ?? {});
|
|
319
321
|
const workspaceId = asString(session.workspaceId);
|
|
320
322
|
if (!workspaceId)
|
|
@@ -322,6 +324,8 @@ export async function resolveSession(opts) {
|
|
|
322
324
|
const repoBranch = asString(session.repoBranch);
|
|
323
325
|
if (!repoBranch)
|
|
324
326
|
throw new Error('session response carried no repoBranch (branch not provisioned)');
|
|
327
|
+
if (session.seedOrigin !== undefined && session.seedOrigin !== 'team' && session.seedOrigin !== 'session')
|
|
328
|
+
throw new Error('session response carried invalid seedOrigin');
|
|
325
329
|
const clientInfo = (session.clientInfo ?? {});
|
|
326
330
|
const initialTask = asString(clientInfo.task);
|
|
327
331
|
if (!initialTask)
|
|
@@ -374,6 +378,8 @@ export async function resolveSession(opts) {
|
|
|
374
378
|
pendingTurn: unfinishedRun ? pendingTurn : null,
|
|
375
379
|
interruptRequestedRunId: asString(session.interruptRequestedRunId),
|
|
376
380
|
agentSessionId: asString(session.agentSessionId),
|
|
381
|
+
// A server with copying switched off omits seedOrigin; no source is bound.
|
|
382
|
+
seedOrigin: session.seedOrigin === 'session' ? 'session' : 'team',
|
|
377
383
|
projectId: asString(session.projectId),
|
|
378
384
|
};
|
|
379
385
|
}
|
|
@@ -383,6 +389,44 @@ export const HOSTED_SESSION_DEFAULT_POLL_MS = 5_000;
|
|
|
383
389
|
export async function waitForNextHostedSessionRead(sleep, pollMs = HOSTED_SESSION_DEFAULT_POLL_MS) {
|
|
384
390
|
await sleep(pollMs);
|
|
385
391
|
}
|
|
392
|
+
function sleepUntil(signal) {
|
|
393
|
+
return (ms) => new Promise(resolve => {
|
|
394
|
+
if (signal.aborted) {
|
|
395
|
+
resolve();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const timer = setTimeout(finish, ms);
|
|
399
|
+
const handleAbort = () => finish();
|
|
400
|
+
function finish() {
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
signal.removeEventListener('abort', handleAbort);
|
|
403
|
+
resolve();
|
|
404
|
+
}
|
|
405
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/** ORI-2200: wait for the next session read, or less when the coordinator says
|
|
409
|
+
* "check now". The read still decides what happens; a check alone does nothing. */
|
|
410
|
+
async function waitForReadOrCheck(opts) {
|
|
411
|
+
if (!opts.live) {
|
|
412
|
+
await waitForNextHostedSessionRead(opts.sleep ?? sleepUntil(opts.signal), opts.pollMs);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const woken = new AbortController();
|
|
416
|
+
const wait = AbortSignal.any([opts.signal, woken.signal]);
|
|
417
|
+
await Promise.race([
|
|
418
|
+
waitForNextHostedSessionRead(opts.sleep ?? sleepUntil(wait), opts.pollMs),
|
|
419
|
+
opts.live.nextCheck(wait),
|
|
420
|
+
]);
|
|
421
|
+
woken.abort();
|
|
422
|
+
}
|
|
423
|
+
/** The sandbox's live-socket pass route sits beside its agent-token route. */
|
|
424
|
+
export function deriveLiveSandboxPassUrl(agentTokenUrl) {
|
|
425
|
+
if (/\/agent-token\/?$/.test(agentTokenUrl)) {
|
|
426
|
+
return agentTokenUrl.replace(/\/agent-token\/?$/, '/live/sandbox-pass');
|
|
427
|
+
}
|
|
428
|
+
return `${agentTokenUrl.replace(/\/+$/, '')}/live/sandbox-pass`;
|
|
429
|
+
}
|
|
386
430
|
/** Ride the ALI-1757 session/pending-turn poll while one prompt is active.
|
|
387
431
|
* This uses the same additive session GET contract, but deliberately shares
|
|
388
432
|
* none of the between-turn credential cleanup or mint-on-401 behavior: the
|
|
@@ -396,20 +440,6 @@ export async function pollHostedSessionDuringTurn(opts) {
|
|
|
396
440
|
};
|
|
397
441
|
const readFile = opts.readFile ?? ((path) => readFileSync(path, 'utf8'));
|
|
398
442
|
const pollMs = opts.pollMs ?? HOSTED_SESSION_DEFAULT_POLL_MS;
|
|
399
|
-
const sleep = opts.sleep ?? ((ms) => new Promise(resolve => {
|
|
400
|
-
if (opts.signal.aborted) {
|
|
401
|
-
resolve();
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const timer = setTimeout(finish, ms);
|
|
405
|
-
const handleAbort = () => finish();
|
|
406
|
-
function finish() {
|
|
407
|
-
clearTimeout(timer);
|
|
408
|
-
opts.signal.removeEventListener('abort', handleAbort);
|
|
409
|
-
resolve();
|
|
410
|
-
}
|
|
411
|
-
opts.signal.addEventListener('abort', handleAbort, { once: true });
|
|
412
|
-
}));
|
|
413
443
|
while (!opts.signal.aborted) {
|
|
414
444
|
try {
|
|
415
445
|
const latest = await resolveSession({
|
|
@@ -444,8 +474,9 @@ export async function pollHostedSessionDuringTurn(opts) {
|
|
|
444
474
|
// enter logs. A transient read failure cannot fail the running turn.
|
|
445
475
|
diagnose('hosted interrupt poll failed; retrying');
|
|
446
476
|
}
|
|
447
|
-
if (!opts.signal.aborted)
|
|
448
|
-
await
|
|
477
|
+
if (!opts.signal.aborted) {
|
|
478
|
+
await waitForReadOrCheck({ signal: opts.signal, pollMs, sleep: opts.sleep, live: opts.live });
|
|
479
|
+
}
|
|
449
480
|
}
|
|
450
481
|
}
|
|
451
482
|
/** Reuse an existing run or start one with the AGENT bearer (actor_type='agent'),
|
|
@@ -498,7 +529,7 @@ export async function resolveTeamRemote(opts) {
|
|
|
498
529
|
const url = `${opts.baseUrl}/api/cli/workspaces/${encodeURIComponent(opts.workspaceId)}/repo-token`;
|
|
499
530
|
const minted = await bearerJson(opts.fetchImpl, url, opts.bearer, {
|
|
500
531
|
method: 'POST',
|
|
501
|
-
headers: { 'Content-Type': 'application/json' },
|
|
532
|
+
headers: { 'Content-Type': 'application/json', 'X-Orizu-Session-Seed-Protocol': '1' },
|
|
502
533
|
body: JSON.stringify({ purpose: 'team_read', sessionId: opts.sessionId }),
|
|
503
534
|
});
|
|
504
535
|
const remote = asString(minted.remote);
|
|
@@ -552,6 +583,30 @@ const defaultExec = (cmd, args, opts) => {
|
|
|
552
583
|
const res = spawnSync(cmd, args, { cwd: opts?.cwd, encoding: 'utf8' });
|
|
553
584
|
return { status: res.status ?? 1, stdout: res.stdout ?? '', stderr: res.stderr ?? '' };
|
|
554
585
|
};
|
|
586
|
+
/** ORI-2520: the repo on a woken disk must be this session's: a repository
|
|
587
|
+
* at the folder's top with the session's remote and a commit checked out
|
|
588
|
+
* (detached is fine). Which branch is checked out is not checked: each
|
|
589
|
+
* turn's save pushes the current commit to the session branch whatever is
|
|
590
|
+
* checked out, so an agent may have left another branch (or none) checked
|
|
591
|
+
* out. Returns that branch for the log. */
|
|
592
|
+
function assertWokenRepo(exec, workspaceDir, remote) {
|
|
593
|
+
const read = (...args) => {
|
|
594
|
+
const result = exec('git', ['-C', workspaceDir, ...args]);
|
|
595
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
596
|
+
};
|
|
597
|
+
if (read('rev-parse', '--git-dir') !== '.git') {
|
|
598
|
+
throw new Error('woken disk: the repo folder holds no repository of its own');
|
|
599
|
+
}
|
|
600
|
+
// The configured value, not `remote get-url`, which applies url rewrites.
|
|
601
|
+
if (read('config', '--get', 'remote.origin.url') !== remote) {
|
|
602
|
+
throw new Error("woken disk: the repo's remote is not this session's");
|
|
603
|
+
}
|
|
604
|
+
// A clone cut off before any commit was checked out has no saved work.
|
|
605
|
+
if (read('rev-parse', '--verify', '--quiet', 'HEAD^{commit}') === null) {
|
|
606
|
+
throw new Error('woken disk: the repo has no commit checked out');
|
|
607
|
+
}
|
|
608
|
+
return read('rev-parse', '--abbrev-ref', 'HEAD') ?? 'no branch';
|
|
609
|
+
}
|
|
555
610
|
/** Values interpolated into git's credential.helper string / clone args must be
|
|
556
611
|
* benign. We pass everything via execFile-style arg arrays (no shell), but
|
|
557
612
|
* still reject anything with newlines/control chars as defense in depth. */
|
|
@@ -588,6 +643,20 @@ class HostedTurnFailure extends Error {
|
|
|
588
643
|
* provider fed by the same pull-mode source. Returns the loop's terminal status.
|
|
589
644
|
*/
|
|
590
645
|
export async function runHostedBoot(opts) {
|
|
646
|
+
// ORI-2200: the sandbox's live socket, opened once the boot first waits for
|
|
647
|
+
// work and closed however the boot ends.
|
|
648
|
+
const liveChannels = [];
|
|
649
|
+
const idleWait = new AbortController();
|
|
650
|
+
try {
|
|
651
|
+
return await runHostedBootWithLive(opts, liveChannels, idleWait.signal);
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
idleWait.abort();
|
|
655
|
+
for (const channel of liveChannels)
|
|
656
|
+
channel.close();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async function runHostedBootWithLive(opts, liveChannels, idleWaitSignal) {
|
|
591
660
|
const env = opts.env;
|
|
592
661
|
const processEnv = opts.processEnv ?? process.env;
|
|
593
662
|
const root = opts.root ?? process.cwd();
|
|
@@ -668,6 +737,22 @@ export async function runHostedBoot(opts) {
|
|
|
668
737
|
attemptTimeoutMs: turnStatusAttemptTimeoutMs,
|
|
669
738
|
});
|
|
670
739
|
}
|
|
740
|
+
// The coordinator says "check now" over this socket when someone presses
|
|
741
|
+
// Stop or sends a message, so the waits below need not run their full
|
|
742
|
+
// interval. Best effort: without it the regular reads still run.
|
|
743
|
+
let liveChannel = null;
|
|
744
|
+
const openLive = () => {
|
|
745
|
+
if (!liveChannel) {
|
|
746
|
+
liveChannel = openHostedLiveChannel({
|
|
747
|
+
sandboxPassUrl: deriveLiveSandboxPassUrl(env.agentTokenUrl),
|
|
748
|
+
bootSecret: env.bootSecret,
|
|
749
|
+
fetchImpl,
|
|
750
|
+
log,
|
|
751
|
+
});
|
|
752
|
+
liveChannels.push(liveChannel);
|
|
753
|
+
}
|
|
754
|
+
return liveChannel;
|
|
755
|
+
};
|
|
671
756
|
let consecutiveIdleSessionReadFailures = 0;
|
|
672
757
|
const recordIdleSessionReadFailure = (detail) => {
|
|
673
758
|
consecutiveIdleSessionReadFailures += 1;
|
|
@@ -677,7 +762,12 @@ export async function runHostedBoot(opts) {
|
|
|
677
762
|
log(`idle session resolution failed; retrying (${detail})`);
|
|
678
763
|
};
|
|
679
764
|
while (!opts.runLoop && !currentNonTerminalRunId) {
|
|
680
|
-
await
|
|
765
|
+
await waitForReadOrCheck({
|
|
766
|
+
signal: idleWaitSignal,
|
|
767
|
+
pollMs: Math.min(idlePollMs * 2 ** consecutiveIdleSessionReadFailures, idleReadBackoffCapMs),
|
|
768
|
+
sleep: opts.sleep,
|
|
769
|
+
live: openLive(),
|
|
770
|
+
});
|
|
681
771
|
try {
|
|
682
772
|
if (bearer.expiresAtMs !== null && bearer.expiresAtMs <= now() + 60_000) {
|
|
683
773
|
bearer = await pullAgentBearer({
|
|
@@ -786,54 +876,149 @@ export async function runHostedBoot(opts) {
|
|
|
786
876
|
writeFile(bootContextAbs, serializeBootContext(bootContext));
|
|
787
877
|
const helperValue = `!node ${helperScriptAbs} ${bootContextAbs}`;
|
|
788
878
|
// 4b — ORI-2118: a hosted session's Artifacts copy is created empty (forks
|
|
789
|
-
// of a busy team copy cannot be cloned)
|
|
790
|
-
// before
|
|
879
|
+
// of a busy team copy cannot be cloned). Fill it from the server-selected
|
|
880
|
+
// origin before cloning. Selected saved history is checked on retries too.
|
|
881
|
+
if (session.seedOrigin === 'session' && !isValidCloudflareArtifactsGitRemote(repo.cloneUrl)) {
|
|
882
|
+
throw new Error('selected session source requires an admitted Artifacts destination');
|
|
883
|
+
}
|
|
791
884
|
if (isValidCloudflareArtifactsGitRemote(repo.cloneUrl)) {
|
|
792
885
|
const credentialConfig = (helper) => [`credential.helper=${helper}`, 'credential.useHttpPath=true'];
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
886
|
+
const sourceRequestId = session.seedOrigin === 'session' ? randomUUID() : null;
|
|
887
|
+
const sourceCredentialFile = `${sessionDirAbs}/seed-source-credential`;
|
|
888
|
+
const sourceHelperFile = `${sessionDirAbs}/seed-source-helper.js`;
|
|
889
|
+
let seedFailed = false;
|
|
890
|
+
let seedError = null;
|
|
891
|
+
try {
|
|
892
|
+
await seedSessionCopy({
|
|
893
|
+
seedOrigin: session.seedOrigin,
|
|
894
|
+
exec,
|
|
895
|
+
sessionRemote: repo.cloneUrl,
|
|
896
|
+
sessionGitConfig: credentialConfig(helperValue),
|
|
897
|
+
scratchDir: `${sessionDirAbs}/seed.git`,
|
|
898
|
+
resolveTeam: async () => {
|
|
899
|
+
if (sourceRequestId) {
|
|
900
|
+
const url = `${env.baseUrl}/api/cli/workspaces/${encodeURIComponent(workspaceId)}/repo-token`;
|
|
901
|
+
const credential = await bearerJson(fetchImpl, url, bearer.token, {
|
|
902
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
903
|
+
body: JSON.stringify({ purpose: 'source_read', sessionId: env.sessionId, requestId: sourceRequestId }),
|
|
904
|
+
});
|
|
905
|
+
const remote = asString(credential.remote);
|
|
906
|
+
const token = asString(credential.token);
|
|
907
|
+
const username = asString(credential.username);
|
|
908
|
+
if (credential.provider !== 'cloudflare_artifacts' || !remote || !isValidCloudflareArtifactsGitRemote(remote) || !token || !username || credential.requestId !== sourceRequestId) {
|
|
909
|
+
throw new Error('source_read response carried no valid admitted source credential');
|
|
910
|
+
}
|
|
911
|
+
assertSafeGitValue('sourceRemote', remote);
|
|
912
|
+
writeSecretFile(sourceCredentialFile, JSON.stringify({ remote, username, token }), writeFile);
|
|
913
|
+
writeFile(sourceHelperFile, String.raw `const fs = require('fs');
|
|
914
|
+
const c = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
915
|
+
let input = '';
|
|
916
|
+
process.stdin.on('data', d => input += d);
|
|
917
|
+
process.stdin.on('end', () => {
|
|
918
|
+
const fields = Object.fromEntries(input.trim().split('\n').map(line => {
|
|
919
|
+
const i = line.indexOf('='); return [line.slice(0, i), line.slice(i + 1)];
|
|
920
|
+
}));
|
|
921
|
+
const u = new URL(c.remote);
|
|
922
|
+
if (process.argv[3] === 'get' && fields.protocol === 'https' && fields.host === u.host && fields.path === u.pathname.slice(1)) {
|
|
923
|
+
process.stdout.write('username=' + c.username + '\npassword=' + c.token + '\n\n');
|
|
924
|
+
}
|
|
925
|
+
});`);
|
|
926
|
+
return { remote, gitConfig: ['credential.helper=', `credential.helper=!node ${sourceHelperFile} ${sourceCredentialFile}`, 'credential.useHttpPath=true'] };
|
|
927
|
+
}
|
|
928
|
+
const teamRemote = await resolveTeamRemote({ baseUrl: env.baseUrl, workspaceId, sessionId: env.sessionId, bearer: bearer.token, fetchImpl });
|
|
929
|
+
assertSafeGitValue('teamRemote', teamRemote);
|
|
930
|
+
const teamContextAbs = `${sessionDirAbs}/${TEAM_BOOT_CONTEXT_BASENAME}`;
|
|
931
|
+
writeFile(teamContextAbs, serializeBootContext({
|
|
932
|
+
...bootContext,
|
|
933
|
+
repoFullName: teamRemote,
|
|
934
|
+
host: new URL(teamRemote).host.toLowerCase(),
|
|
935
|
+
tokenPurposes: { primary: 'team_read', fallback: 'team_read' },
|
|
936
|
+
}));
|
|
937
|
+
return { remote: teamRemote, gitConfig: credentialConfig(`!node ${helperScriptAbs} ${teamContextAbs}`) };
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
seedFailed = true;
|
|
943
|
+
seedError = error;
|
|
944
|
+
}
|
|
945
|
+
// Always request cleanup, including lost mint replies. The server tracks
|
|
946
|
+
// the request before issuance and refuses uncertain cleanup.
|
|
947
|
+
let cleanupFailed = false;
|
|
948
|
+
let cleanupError = null;
|
|
949
|
+
try {
|
|
950
|
+
try {
|
|
951
|
+
if (sourceRequestId) {
|
|
952
|
+
const cleanup = await bearerJson(fetchImpl, `${env.baseUrl}/api/cli/workspaces/${encodeURIComponent(workspaceId)}/repo-token`, bearer.token, {
|
|
953
|
+
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
|
|
954
|
+
body: JSON.stringify({ purpose: 'source_read', sessionId: env.sessionId, requestId: sourceRequestId }),
|
|
955
|
+
});
|
|
956
|
+
if (cleanup.settled !== true || cleanup.outcome !== 'revoked') {
|
|
957
|
+
throw new Error('temporary source credential revocation could not be proved');
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
finally {
|
|
962
|
+
if (sourceRequestId) {
|
|
963
|
+
const removed = exec('rm', ['-f', '--', sourceCredentialFile, sourceHelperFile]);
|
|
964
|
+
if (removed.status !== 0)
|
|
965
|
+
throw new Error('temporary source credential cleanup failed');
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
cleanupFailed = true;
|
|
971
|
+
cleanupError = error;
|
|
972
|
+
}
|
|
973
|
+
// A failed copy keeps its own reason; a cleanup failure is added, never
|
|
974
|
+
// substituted, so the person sees why the copy stopped.
|
|
975
|
+
if (seedFailed && cleanupFailed) {
|
|
976
|
+
const reason = (error) => error instanceof Error ? error.message : String(error);
|
|
977
|
+
throw new Error(`${reason(seedError)}; temporary source credential cleanup also failed: ${reason(cleanupError)}`);
|
|
978
|
+
}
|
|
979
|
+
if (seedFailed)
|
|
980
|
+
throw seedError;
|
|
981
|
+
if (cleanupFailed)
|
|
982
|
+
throw cleanupError;
|
|
811
983
|
}
|
|
812
984
|
// 5 — Clone the session branch VIA the pull-mode credential helper (same
|
|
813
985
|
// invocation the operator path makes; no token in the URL/config).
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
986
|
+
// ORI-2520: a woken sandbox boots on the disk it slept with, so this
|
|
987
|
+
// session's repo is already here with any work since the last push. It is
|
|
988
|
+
// reused; anything else in that folder is refused, never overwritten. An
|
|
989
|
+
// empty folder is a fresh disk (the runtime image ships one) and is cloned
|
|
990
|
+
// into.
|
|
991
|
+
const wokenDisk = existsSync(workspaceDir) && readdirSync(workspaceDir).length > 0;
|
|
992
|
+
if (wokenDisk) {
|
|
993
|
+
const checkedOut = assertWokenRepo(exec, workspaceDir, repo.cloneUrl);
|
|
994
|
+
log(`woken disk: reusing the repo (checked out: ${checkedOut})`);
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
const clone = exec('git', [
|
|
998
|
+
'clone',
|
|
999
|
+
'--depth',
|
|
1000
|
+
'1',
|
|
1001
|
+
'--branch',
|
|
1002
|
+
session.repoBranch,
|
|
1003
|
+
'-c',
|
|
1004
|
+
`credential.helper=${helperValue}`,
|
|
1005
|
+
'-c',
|
|
1006
|
+
'credential.useHttpPath=true',
|
|
1007
|
+
repo.cloneUrl,
|
|
1008
|
+
workspaceDir,
|
|
1009
|
+
]);
|
|
1010
|
+
if (clone.status !== 0) {
|
|
1011
|
+
const detail = (clone.stderr || clone.stdout || `exit ${clone.status}`).trim();
|
|
1012
|
+
throw new Error(`git clone failed: ${detail}`);
|
|
1013
|
+
}
|
|
830
1014
|
}
|
|
831
1015
|
// Persist the helper + agent identity repo-LOCAL for subsequent fetch/push.
|
|
832
1016
|
exec('git', ['-C', workspaceDir, 'config', 'credential.helper', helperValue]);
|
|
833
1017
|
exec('git', ['-C', workspaceDir, 'config', 'credential.useHttpPath', 'true']);
|
|
834
1018
|
exec('git', ['-C', workspaceDir, 'config', 'user.name', AGENT_GIT_IDENTITY.name]);
|
|
835
1019
|
exec('git', ['-C', workspaceDir, 'config', 'user.email', AGENT_GIT_IDENTITY.email]);
|
|
836
|
-
|
|
1020
|
+
if (!wokenDisk)
|
|
1021
|
+
log(`cloned ${session.repoBranch}`);
|
|
837
1022
|
// 5b — Stage the orizu skill into the cloned repo so the agent discovers the
|
|
838
1023
|
// Orizu workflows (ALI-1059). SHARED with the operator path via `stageOrizuSkill`
|
|
839
1024
|
// (one resolution chain + the harvest-safe .git/info/exclude append). Non-fatal:
|
|
@@ -953,6 +1138,7 @@ export async function runHostedBoot(opts) {
|
|
|
953
1138
|
let currentBearer = bearer;
|
|
954
1139
|
let lastResult = { status: 'succeeded', agentSessionId: session.agentSessionId, installOk: true, error: null };
|
|
955
1140
|
let hostedTurnFailure = null;
|
|
1141
|
+
const live = openLive();
|
|
956
1142
|
try {
|
|
957
1143
|
for (;;) {
|
|
958
1144
|
assertSafeRunId(current.runId);
|
|
@@ -996,6 +1182,7 @@ export async function runHostedBoot(opts) {
|
|
|
996
1182
|
onDiagnostic: log,
|
|
997
1183
|
readFile,
|
|
998
1184
|
pollMs: idlePollMs,
|
|
1185
|
+
live,
|
|
999
1186
|
});
|
|
1000
1187
|
try {
|
|
1001
1188
|
lastResult = await runHostedLoopTurn(hostedLoopSession, {
|
|
@@ -1054,8 +1241,14 @@ export async function runHostedBoot(opts) {
|
|
|
1054
1241
|
// budget. Refresh proactively near expiry and once on a 401. Exhausting
|
|
1055
1242
|
// the bounded agent-token pull is fatal and reported for the current run;
|
|
1056
1243
|
// other transient status-read failures retain the session and back off.
|
|
1244
|
+
// ORI-2517: the first read after a turn runs at once. A message the
|
|
1245
|
+
// person sent during the turn or its save, or a check that came in
|
|
1246
|
+
// meanwhile, is already waiting.
|
|
1247
|
+
let isFirstIdleRead = true;
|
|
1057
1248
|
for (;;) {
|
|
1058
|
-
|
|
1249
|
+
if (!isFirstIdleRead)
|
|
1250
|
+
await waitForReadOrCheck({ signal: idleWaitSignal, pollMs: idlePollMs, sleep: opts.sleep, live });
|
|
1251
|
+
isFirstIdleRead = false;
|
|
1059
1252
|
let pendingTurn = null;
|
|
1060
1253
|
try {
|
|
1061
1254
|
if (currentBearer.expiresAtMs !== null && currentBearer.expiresAtMs <= now() + 60_000) {
|
package/dist/hosted-harness.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export declare const HARNESS_EVENT_KINDS: {
|
|
|
73
73
|
readonly session_title: "session_title";
|
|
74
74
|
readonly egress_blocked: "egress_blocked";
|
|
75
75
|
readonly egress_allowed: "egress_allowed";
|
|
76
|
+
readonly work_saving: "work_saving";
|
|
76
77
|
readonly work_persisted: "work_persisted";
|
|
77
78
|
readonly work_none: "work_none";
|
|
78
79
|
readonly work_not_inspected: "work_not_inspected";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hosted-harness.d.ts","sourceRoot":"","sources":["../src/hosted-harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;8DAC8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;IACvC,wEAAwE;IACxE,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CACxC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB
|
|
1
|
+
{"version":3,"file":"hosted-harness.d.ts","sourceRoot":"","sources":["../src/hosted-harness.ts"],"names":[],"mappings":"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"}
|
package/dist/hosted-harness.js
CHANGED
|
@@ -51,6 +51,8 @@ export const HARNESS_EVENT_KINDS = {
|
|
|
51
51
|
// the end-of-run auto-harvest result (persisted/none/not-inspected/failed)
|
|
52
52
|
// and the headless question auto-handling record (question_auto_answered). They
|
|
53
53
|
// are ordinary structural events (never terminal), appended like any other.
|
|
54
|
+
// work_saving (ORI-2517) marks the model done and the save starting.
|
|
55
|
+
work_saving: 'work_saving',
|
|
54
56
|
work_persisted: 'work_persisted',
|
|
55
57
|
work_none: 'work_none',
|
|
56
58
|
work_not_inspected: 'work_not_inspected',
|
package/dist/hosted-harvest.d.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 1. `git status --porcelain` (plumbing) decides if the clone is dirty. Ignored
|
|
9
9
|
* files never appear in porcelain output, so a checkpoint is skipped when the
|
|
10
10
|
* only changes are ignored files.
|
|
11
|
-
* 2. If dirty: `git add -A
|
|
11
|
+
* 2. If dirty: `git add -A`, unstage the staged Orizu skill links
|
|
12
|
+
* (orizu-skill-links.ts), and if anything is still staged, `git commit` attributed to the agent identity
|
|
12
13
|
* (AGENT_GIT_IDENTITY) with message `checkpoint: run <runId> auto-harvest`.
|
|
13
14
|
* 3. If there is anything unpushed (a fresh checkpoint OR pre-existing local
|
|
14
15
|
* commits the agent made but did not push), push the exact HEAD SHA to the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hosted-harvest.d.ts","sourceRoot":"","sources":["../src/hosted-harvest.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"hosted-harvest.d.ts","sourceRoot":"","sources":["../src/hosted-harvest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAcH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAED,iFAAiF;AACjF,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,iBAAiB,CAAA;AAExE,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,aAAa,EAAE,MAAM,CAAA;IACrB,+DAA+D;IAC/D,gBAAgB,EAAE,MAAM,CAAA;IACxB,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,0EAA0E;IAC1E,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACvC,kFAAkF;IAClF,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,qEAAqE;IACrE,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,oBAAoB,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAElD,eAAO,MAAM,mCAAmC,0GAItC,CAAA;AAEV,eAAO,MAAM,iCAAiC,+EAGpC,CAAA;AAEV,eAAO,MAAM,+BAA+B;;;;CAgBlC,CAAA;AAUV,wBAAgB,4BAA4B,CAC1C,UAAU,GAAE,SAAS,MAAM,EAAwC,GAClE,MAAM,GAAG,IAAI,CAEf;AA6GD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CA8KrE"}
|
package/dist/hosted-harvest.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 1. `git status --porcelain` (plumbing) decides if the clone is dirty. Ignored
|
|
9
9
|
* files never appear in porcelain output, so a checkpoint is skipped when the
|
|
10
10
|
* only changes are ignored files.
|
|
11
|
-
* 2. If dirty: `git add -A
|
|
11
|
+
* 2. If dirty: `git add -A`, unstage the staged Orizu skill links
|
|
12
|
+
* (orizu-skill-links.ts), and if anything is still staged, `git commit` attributed to the agent identity
|
|
12
13
|
* (AGENT_GIT_IDENTITY) with message `checkpoint: run <runId> auto-harvest`.
|
|
13
14
|
* 3. If there is anything unpushed (a fresh checkpoint OR pre-existing local
|
|
14
15
|
* commits the agent made but did not push), push the exact HEAD SHA to the
|
|
@@ -30,6 +31,7 @@ import { mkdtempSync, readdirSync, rmSync, statSync } from 'fs';
|
|
|
30
31
|
import { tmpdir } from 'os';
|
|
31
32
|
import { join } from 'path';
|
|
32
33
|
import { isValidCloudflareArtifactsGitRemote, parseCanonicalCredentialFreeHttpsUrl, } from './cloudflare-artifacts-git-remote.js';
|
|
34
|
+
import { hasStagedChanges, NOTHING_STAGED_ERROR, unstageOrizuSkillLinks } from './orizu-skill-links.js';
|
|
33
35
|
export const HOSTED_HARVEST_CA_BUNDLE_CANDIDATES = [
|
|
34
36
|
'/etc/pki/tls/certs/ca-bundle.crt',
|
|
35
37
|
'/etc/ssl/certs/ca-certificates.crt',
|
|
@@ -224,31 +226,41 @@ export function harvestWorkspace(opts) {
|
|
|
224
226
|
return { kind: 'work_persist_failed', error: 'hosted_harvest_ca_path_policy_ineffective' };
|
|
225
227
|
}
|
|
226
228
|
}
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
const
|
|
232
|
-
const status = exec(['status', '--porcelain', '--', '.', ...excludeScaffold]);
|
|
229
|
+
// `git status` is only the cheap first look; whether there is anything to
|
|
230
|
+
// save is decided from the index after the staged skill links are taken
|
|
231
|
+
// back out (ALI-1051, ORI-2442). A sandbox where the ignore lines did not
|
|
232
|
+
// run shows the links here and still saves nothing.
|
|
233
|
+
const status = exec(['status', '--porcelain']);
|
|
233
234
|
if (status.exitCode !== 0) {
|
|
234
235
|
return { kind: 'work_persist_failed', error: `git status failed: ${detail(status)}` };
|
|
235
236
|
}
|
|
236
|
-
|
|
237
|
+
let dirty = status.stdout.trim().length > 0;
|
|
237
238
|
if (dirty) {
|
|
238
|
-
// Add everything, then unstage the staged skill
|
|
239
|
-
// pathspec on add makes git exit 1
|
|
240
|
-
// .
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
// reset, unlike rm --cached, never deletes a tracked file.
|
|
239
|
+
// Add everything, then unstage the staged skill links. An exclude
|
|
240
|
+
// pathspec on add makes git exit 1 for ignored paths (ORI-2441, git 2.49
|
|
241
|
+
// in the sandbox). Only the links themselves are reset, so an agent's
|
|
242
|
+
// edits to a team's own skills next to them are saved; reset, unlike
|
|
243
|
+
// rm --cached, never deletes a tracked file.
|
|
244
244
|
const add = exec(['add', '-A', '--', '.']);
|
|
245
245
|
if (add.exitCode !== 0) {
|
|
246
246
|
return { kind: 'work_persist_failed', error: `git add failed: ${detail(add)}` };
|
|
247
247
|
}
|
|
248
|
-
const unstage = exec
|
|
249
|
-
if (unstage.
|
|
250
|
-
return { kind: 'work_persist_failed', error:
|
|
248
|
+
const unstage = unstageOrizuSkillLinks(exec);
|
|
249
|
+
if (!unstage.ok) {
|
|
250
|
+
return { kind: 'work_persist_failed', error: unstage.error };
|
|
251
251
|
}
|
|
252
|
+
const staged = hasStagedChanges(exec);
|
|
253
|
+
if (typeof staged !== 'boolean') {
|
|
254
|
+
return { kind: 'work_persist_failed', error: staged.error };
|
|
255
|
+
}
|
|
256
|
+
// Dirty with nothing staged and no link taken out means the work cannot
|
|
257
|
+
// be committed here; say so rather than report nothing to save.
|
|
258
|
+
if (!staged && unstage.unstaged.length === 0) {
|
|
259
|
+
return { kind: 'work_persist_failed', error: NOTHING_STAGED_ERROR };
|
|
260
|
+
}
|
|
261
|
+
dirty = staged;
|
|
262
|
+
}
|
|
263
|
+
if (dirty) {
|
|
252
264
|
const commit = exec([
|
|
253
265
|
'-c',
|
|
254
266
|
`user.name=${opts.author.name}`,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORI-2200 slice 2 (ADR-040): the sandbox's socket to its session coordinator.
|
|
3
|
+
* The coordinator sends "check now" when someone presses Stop; the sandbox
|
|
4
|
+
* then reads its session at once instead of waiting for the next regular read.
|
|
5
|
+
* The message carries no instruction of its own: the session record decides
|
|
6
|
+
* whether this run was interrupted, so a forged or stray check costs one read.
|
|
7
|
+
*
|
|
8
|
+
* Best effort. If the socket cannot open (an older coordinator, a network
|
|
9
|
+
* rule), the regular read every few seconds still catches the Stop.
|
|
10
|
+
*/
|
|
11
|
+
export interface HostedLiveChannel {
|
|
12
|
+
/** Resolves on the next "check now" (or at once if one arrived since the last call), or when `signal` aborts. */
|
|
13
|
+
nextCheck(signal: AbortSignal): Promise<void>;
|
|
14
|
+
/** How many checks have arrived so far, read or not. */
|
|
15
|
+
checkCount(): number;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
18
|
+
export declare function openHostedLiveChannel(opts: {
|
|
19
|
+
/** `<coordinator>/sessions/<id>/live/sandbox-pass`. */
|
|
20
|
+
sandboxPassUrl: string;
|
|
21
|
+
bootSecret: string;
|
|
22
|
+
fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;
|
|
23
|
+
log?: (line: string) => void;
|
|
24
|
+
}): HostedLiveChannel;
|
|
25
|
+
//# sourceMappingURL=hosted-live-channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-live-channel.d.ts","sourceRoot":"","sources":["../src/hosted-live-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,MAAM,WAAW,iBAAiB;IAChC,iHAAiH;IACjH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,wDAAwD;IACxD,UAAU,IAAI,MAAM,CAAA;IACpB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClE,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7B,GAAG,iBAAiB,CAwGpB"}
|