@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/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
- | { kind: "gh.connect.clear"; userId: string }
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
- | { kind: "auth"; token: string; hostId: string }
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
- | { kind: "event"; event: HostEvent }
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
- | { kind: "gh.connect.ack"; userId: string; ok: false; error: string }
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 }