@runuai/host 0.9.11 → 0.9.13
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/db/migrations/0013_github_credential_generations.sql +11 -0
- package/db/migrations/meta/_journal.json +7 -0
- package/db/schema.ts +18 -0
- package/lib/env.ts +6 -1
- package/lib/github-tokens.ts +633 -69
- package/package.json +1 -1
- package/src/event-outbox.ts +104 -0
- package/src/main.ts +299 -29
- package/src/protocol.ts +123 -6
package/src/protocol.ts
CHANGED
|
@@ -35,11 +35,28 @@ export type HostCommandResult<T> =
|
|
|
35
35
|
export const TRANSCRIPT_TARGETS_PROTOCOL_FEATURE = "transcript-targets-v1";
|
|
36
36
|
export const SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE =
|
|
37
37
|
"secretary-typed-dispatch-v1";
|
|
38
|
+
export const GITHUB_INSTALLATION_VERIFICATION_PROTOCOL_FEATURE =
|
|
39
|
+
"github-installation-verification-v1";
|
|
40
|
+
export const GITHUB_REPOSITORY_ACCESS_PROTOCOL_FEATURE =
|
|
41
|
+
"github-repository-access-v1";
|
|
42
|
+
export const GITHUB_CREDENTIAL_GENERATION_PROTOCOL_FEATURE =
|
|
43
|
+
"github-credential-generation-v1";
|
|
44
|
+
// ADR-103: the host runs an event outbox — seq'd event frames, resume
|
|
45
|
+
// handshake on reconnect, cumulative acks. A cloud seeing this feature knows
|
|
46
|
+
// a disconnect no longer loses events (they replay), so it keeps its
|
|
47
|
+
// in-flight turn buffers across the gap.
|
|
48
|
+
export const EVENT_REPLAY_PROTOCOL_FEATURE = "event-replay-v1";
|
|
38
49
|
export const COMMUNICATOR_EXECUTION_PROFILE = "communicator";
|
|
39
50
|
export const MAX_AGENT_ID_CHARS = 128;
|
|
40
51
|
export const MAX_SECRETARY_DISPATCH_RECIPIENTS = 16;
|
|
41
52
|
export const MAX_SECRETARY_DISPATCH_INSTRUCTION_CHARS = 8_000;
|
|
42
53
|
export const MAX_SECRETARY_DISPATCH_ID_CHARS = 256;
|
|
54
|
+
// Wire and GitHub-call bounds for the host-assisted authorization snapshot.
|
|
55
|
+
// Exceeding either bound fails the whole verification; a partial installation
|
|
56
|
+
// list could cause destructive pruning, while a partial repository list is an
|
|
57
|
+
// explicit false-negative allowlist and therefore carries `truncated: true`.
|
|
58
|
+
export const MAX_GITHUB_INSTALLATIONS = 100;
|
|
59
|
+
export const MAX_GITHUB_REPOSITORIES_PER_INSTALLATION = 1_000;
|
|
43
60
|
|
|
44
61
|
export interface CommandContext {
|
|
45
62
|
commandId: string;
|
|
@@ -133,6 +150,19 @@ export function parseChannelMode(
|
|
|
133
150
|
throw new Error("invalid command args: expected channel mode");
|
|
134
151
|
}
|
|
135
152
|
|
|
153
|
+
/** Optional during rolling upgrades; `null` is the explicit invalid sentinel
|
|
154
|
+
* so generation zero remains a valid migrated credential generation. */
|
|
155
|
+
export function parseGitHubCredentialGeneration(
|
|
156
|
+
value: unknown,
|
|
157
|
+
): number | undefined | null {
|
|
158
|
+
if (value === undefined) return undefined;
|
|
159
|
+
return typeof value === "number" &&
|
|
160
|
+
Number.isSafeInteger(value) &&
|
|
161
|
+
value >= 0
|
|
162
|
+
? value
|
|
163
|
+
: null;
|
|
164
|
+
}
|
|
165
|
+
|
|
136
166
|
/** Parse the appendTranscript wire argument. `undefined` alone is the legacy
|
|
137
167
|
* three-argument command and maps to chat.md during a rolling deploy. */
|
|
138
168
|
export function parseTranscriptTargets(value: unknown): TranscriptTarget[] {
|
|
@@ -468,6 +498,13 @@ export interface TunnelReqLine {
|
|
|
468
498
|
|
|
469
499
|
export type TunnelTarget = "editor" | "preview";
|
|
470
500
|
|
|
501
|
+
export type GitHubRepositoryAccessErrorCode =
|
|
502
|
+
| "token_missing"
|
|
503
|
+
| "reauth_required"
|
|
504
|
+
| "installation_inaccessible"
|
|
505
|
+
| "github_unavailable"
|
|
506
|
+
| "invalid_response";
|
|
507
|
+
|
|
471
508
|
export type CloudToHost =
|
|
472
509
|
| {
|
|
473
510
|
kind: "command";
|
|
@@ -506,6 +543,12 @@ export type CloudToHost =
|
|
|
506
543
|
// for short-lived access tokens (host→cloud HTTP POST).
|
|
507
544
|
| {
|
|
508
545
|
kind: "gh.connect.set";
|
|
546
|
+
/** Correlates the stored credential with the exact host acknowledgement.
|
|
547
|
+
* Optional only for rolling compatibility with pre-correlation clouds. */
|
|
548
|
+
opId?: string;
|
|
549
|
+
/** Monotonic cloud-issued mutation fence. Optional only for rolling
|
|
550
|
+
* compatibility; generation-aware clouds require the matching feature. */
|
|
551
|
+
generation?: number;
|
|
509
552
|
userId: string;
|
|
510
553
|
installationId: number;
|
|
511
554
|
githubLogin: string;
|
|
@@ -516,7 +559,31 @@ export type CloudToHost =
|
|
|
516
559
|
}
|
|
517
560
|
// Delete the user's token on the host. ADR-033: the host first POSTs the
|
|
518
561
|
// token to the cloud's /api/github/revoke (kill it at GitHub) before deleting.
|
|
519
|
-
| {
|
|
562
|
+
| {
|
|
563
|
+
kind: "gh.connect.clear";
|
|
564
|
+
opId?: string;
|
|
565
|
+
generation?: number;
|
|
566
|
+
userId: string;
|
|
567
|
+
}
|
|
568
|
+
// Ask the host which installations GitHub currently lists for a user, using
|
|
569
|
+
// the token the host already holds. Used to prune historical owner
|
|
570
|
+
// misbindings before any legacy row is trusted.
|
|
571
|
+
| {
|
|
572
|
+
kind: "gh.installations.list";
|
|
573
|
+
opId: string;
|
|
574
|
+
generation?: number;
|
|
575
|
+
userId: string;
|
|
576
|
+
}
|
|
577
|
+
// Renew one installation's App ∩ user repository allowlist without sending
|
|
578
|
+
// the user's token to the cloud. Kept separate from installation listing so
|
|
579
|
+
// ordinary one-row lease renewal is bounded and independently retryable.
|
|
580
|
+
| {
|
|
581
|
+
kind: "gh.repositories.list";
|
|
582
|
+
opId: string;
|
|
583
|
+
generation?: number;
|
|
584
|
+
userId: string;
|
|
585
|
+
installationId: number;
|
|
586
|
+
}
|
|
520
587
|
// Per-user SSH key lifecycle (ADR-029). Keys are generated + stored on the
|
|
521
588
|
// host; only the public key crosses the bridge (ssh.key.ack). `get` fetches
|
|
522
589
|
// without creating, `ensure` creates-if-absent, `delete` removes.
|
|
@@ -541,7 +608,15 @@ export type CloudToHost =
|
|
|
541
608
|
// discovery, DCR, PKCE, token exchange, encrypted storage. Secrets in a
|
|
542
609
|
// probe (headerValue, clientSecret) are write-only; nothing secret ever
|
|
543
610
|
// rides an ack. `opId` correlates request↔ack.
|
|
544
|
-
| { kind: "mcp.op"; opId: string; op: McpOp }
|
|
611
|
+
| { kind: "mcp.op"; opId: string; op: McpOp }
|
|
612
|
+
// ADR-103: sent once right after auth to a host that supplied a bootId.
|
|
613
|
+
// `afterSeq` is the cloud's replay watermark for this boot: the host
|
|
614
|
+
// replays every outbox entry with seq > afterSeq. `null` means the cloud
|
|
615
|
+
// has no watermark for this boot (fresh boot, or lost KV) — the host sends
|
|
616
|
+
// only entries it never transmitted, replaying nothing.
|
|
617
|
+
| { kind: "event.resume"; afterSeq: number | null }
|
|
618
|
+
// ADR-103: cumulative — the host trims outbox entries with seq <= seq.
|
|
619
|
+
| { kind: "event.ack"; seq: number };
|
|
545
620
|
|
|
546
621
|
/** One MCP-connection operation (ADR-057), executed by the host. */
|
|
547
622
|
export type McpOp =
|
|
@@ -570,14 +645,19 @@ export type McpOp =
|
|
|
570
645
|
| { kind: "disconnect"; connectionId: string };
|
|
571
646
|
|
|
572
647
|
export type HostToCloud =
|
|
573
|
-
|
|
648
|
+
// ADR-103: `bootId` scopes event seqs to one host process lifetime — the
|
|
649
|
+
// cloud's replay watermark only applies within a matching boot. Optional
|
|
650
|
+
// for pre-103 hosts (which also never send seq'd events).
|
|
651
|
+
| { kind: "auth"; token: string; hostId: string; bootId?: string }
|
|
574
652
|
| { kind: "host.capabilities"; capabilities: HostCapabilities }
|
|
575
653
|
| {
|
|
576
654
|
kind: "result";
|
|
577
655
|
commandId: string;
|
|
578
656
|
result: HostCommandResult<unknown>;
|
|
579
657
|
}
|
|
580
|
-
|
|
658
|
+
// ADR-103: `seq` is the outbox sequence number (monotonic within a bootId).
|
|
659
|
+
// Absent from pre-103 hosts; the cloud dedups replay overlap by it.
|
|
660
|
+
| { kind: "event"; event: HostEvent; seq?: number }
|
|
581
661
|
// Host-pushed task lifecycle (ADR-028 local-UI Stop): the host operator paused
|
|
582
662
|
// a task's containers, so the cloud mirrors the status (currently "stopped").
|
|
583
663
|
| { kind: "task.status"; taskId: string; status: string }
|
|
@@ -594,8 +674,45 @@ export type HostToCloud =
|
|
|
594
674
|
| { kind: "tunnel.data"; tunnelId: string }
|
|
595
675
|
| { kind: "tunnel.close"; tunnelId: string; reason?: string }
|
|
596
676
|
// Ack for gh.connect.set / gh.connect.clear (ADR-027).
|
|
597
|
-
| { kind: "gh.connect.ack"; userId: string; ok: true }
|
|
598
|
-
| {
|
|
677
|
+
| { kind: "gh.connect.ack"; opId?: string; userId: string; ok: true }
|
|
678
|
+
| {
|
|
679
|
+
kind: "gh.connect.ack";
|
|
680
|
+
opId?: string;
|
|
681
|
+
userId: string;
|
|
682
|
+
ok: false;
|
|
683
|
+
code?: "stale_generation";
|
|
684
|
+
error: string;
|
|
685
|
+
}
|
|
686
|
+
// Ack for gh.installations.list. `installationIds` is GitHub's current
|
|
687
|
+
// verdict and is authoritative ONLY on ok:true. An error must never be read
|
|
688
|
+
// as "installed nowhere", which would delete every binding the user has.
|
|
689
|
+
| {
|
|
690
|
+
kind: "gh.installations.ack";
|
|
691
|
+
opId: string;
|
|
692
|
+
ok: true;
|
|
693
|
+
installationIds: number[];
|
|
694
|
+
}
|
|
695
|
+
| {
|
|
696
|
+
kind: "gh.installations.ack";
|
|
697
|
+
opId: string;
|
|
698
|
+
ok: false;
|
|
699
|
+
code: GitHubRepositoryAccessErrorCode;
|
|
700
|
+
error: string;
|
|
701
|
+
}
|
|
702
|
+
| {
|
|
703
|
+
kind: "gh.repositories.ack";
|
|
704
|
+
opId: string;
|
|
705
|
+
ok: true;
|
|
706
|
+
repositoryIds: number[];
|
|
707
|
+
truncated: boolean;
|
|
708
|
+
}
|
|
709
|
+
| {
|
|
710
|
+
kind: "gh.repositories.ack";
|
|
711
|
+
opId: string;
|
|
712
|
+
ok: false;
|
|
713
|
+
code: GitHubRepositoryAccessErrorCode;
|
|
714
|
+
error: string;
|
|
715
|
+
}
|
|
599
716
|
// Ack for ssh.key.get / ssh.key.ensure / ssh.key.delete (ADR-029). publicKey
|
|
600
717
|
// is null when the user has no key (after delete, or a get-miss).
|
|
601
718
|
| { kind: "ssh.key.ack"; userId: string; ok: true; publicKey: string | null }
|