@agentconnect.md/setup 1.37.0-rc.3 → 1.37.0-rc.5

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/index.js CHANGED
@@ -36023,20 +36023,6 @@ const ZodRecord = /*@__PURE__*/ $constructor("ZodRecord", (inst, def) => {
36023
36023
  inst.keyType = def.keyType;
36024
36024
  inst.valueType = def.valueType;
36025
36025
  });
36026
- function record$1(keyType, valueType, params) {
36027
- if (!valueType || !valueType._zod) return new ZodRecord({
36028
- type: "record",
36029
- keyType: string(),
36030
- valueType: keyType,
36031
- ...normalizeParams(valueType)
36032
- });
36033
- return new ZodRecord({
36034
- type: "record",
36035
- keyType,
36036
- valueType,
36037
- ...normalizeParams(params)
36038
- });
36039
- }
36040
36026
  function partialRecord(keyType, valueType, params) {
36041
36027
  const k = clone(keyType);
36042
36028
  k._zod.values = void 0;
@@ -36293,1115 +36279,16 @@ function number(params) {
36293
36279
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
36294
36280
  }
36295
36281
  //#endregion
36296
- //#region ../protocol/dist/envelope.js
36282
+ //#region ../protocol/dist/slack-app-manifest.js
36297
36283
  var import_fastify = /* @__PURE__ */ __toESM(require_fastify(), 1);
36298
- object({
36299
- v: literal(1),
36300
- id: string().uuid(),
36301
- ts: string().datetime(),
36302
- type: string(),
36303
- corr: string().uuid().optional(),
36304
- payload: unknown()
36305
- });
36306
- object({
36307
- epoch: number$1().int(),
36308
- agentId: string().uuid().optional(),
36309
- launchId: string().uuid().optional()
36310
- });
36311
- /**
36312
- * Builds the envelope schema for one frame `type` — the `type` literal plus the
36313
- * typed payload — so a wire's discriminated union infers `payload` precisely.
36314
- * Shared by the daemon↔CP union (`frame.ts`) and the relay wire unions
36315
- * (`frames/relay-cp.ts` / `frames/relay-daemon.ts`), which are deliberately
36316
- * SEPARATE unions over the same envelope shape (shared-bot-relay.md §8).
36317
- */
36318
- function frameSchema(type, payload) {
36319
- return object({
36320
- v: literal(1),
36321
- id: string().uuid(),
36322
- ts: string().datetime(),
36323
- type: literal(type),
36324
- corr: string().uuid().optional(),
36325
- payload
36326
- });
36327
- }
36328
- const ErrorFrame = object({
36329
- code: _enum([
36330
- "UNKNOWN_FRAME",
36331
- "FRAME_TOO_LARGE",
36332
- "PROTOCOL_STATE",
36333
- "BAD_PAYLOAD",
36334
- "AUTH_FAILED",
36335
- "ATTESTATION_INVALID",
36336
- "STALE_EPOCH",
36337
- "STALE_LAUNCH",
36338
- "NO_SESSION",
36339
- "SCOPE_DENIED",
36340
- "DELEGATION_DENIED",
36341
- "INVOCATION_CONFLICT",
36342
- "LEASE_EXPIRED",
36343
- "LEASE_DENIED",
36344
- "RATE_LIMITED",
36345
- "CONFLICT",
36346
- "INTERNAL"
36347
- ]),
36348
- message: string(),
36349
- retryable: boolean(),
36350
- details: record$1(string(), unknown()).optional()
36351
- });
36352
- //#endregion
36353
- //#region ../protocol/dist/frames/route.js
36354
- const Platform = string().min(1);
36355
- const SessionKey = object({
36356
- platform: Platform,
36357
- channel: string(),
36358
- thread: string().optional()
36359
- });
36360
- /** Trigger-matching rule for a binding (protocol §5.1). */
36361
- const BindRule = object({ match: discriminatedUnion("kind", [
36362
- object({ kind: literal("mention") }),
36363
- object({ kind: literal("dm") }),
36364
- object({
36365
- kind: literal("keyword"),
36366
- value: string()
36367
- }),
36368
- object({ kind: literal("auto") })
36369
- ]) });
36370
- object({
36371
- sessionKey: SessionKey,
36372
- agentId: string().uuid(),
36373
- workspaceId: string().uuid(),
36374
- bindRules: array(BindRule).default([])
36375
- });
36376
- object({
36377
- ok: boolean(),
36378
- sessionKey: SessionKey,
36379
- reason: string().optional()
36380
- });
36381
- object({
36382
- routingEpoch: number$1().int(),
36383
- rules: array(object({
36384
- match: unknown(),
36385
- agentId: string().uuid()
36386
- }))
36387
- });
36388
- object({
36389
- scope: union([
36390
- object({
36391
- kind: literal("agent"),
36392
- agentId: string().uuid()
36393
- }),
36394
- object({ kind: literal("daemon") }),
36395
- object({
36396
- kind: literal("session"),
36397
- sessionKey: SessionKey
36398
- })
36399
- ]),
36400
- deadline: string().datetime()
36401
- });
36402
- object({
36403
- remaining: number$1().int(),
36404
- drained: array(SessionKey)
36405
- });
36406
- object({ released: array(SessionKey) });
36407
- //#endregion
36408
- //#region ../protocol/dist/frames/integration.js
36409
- /**
36410
- * Platform integration distribution (C→D) — the Slack "install" flow.
36411
- *
36412
- * The Control Plane is the source of truth for platform integrations and pushes
36413
- * them to the daemon that owns the integration's agent (`integration/upsert`, and
36414
- * the reconcile snapshot `RegisterOk.integrations[]`). The daemon opens the Socket
36415
- * Mode connection from the delivered config (see slack/connection.ts).
36416
- *
36417
- * SECURITY: `integration/upsert` and `RegisterOk.integrations[]` carry PLAINTEXT
36418
- * platform tokens (botToken/appToken/appSecret). These payloads MUST NEVER be
36419
- * logged — no body dump on decode error, no register/ok snapshot debug dump.
36420
- * CP-owned integrations live only in daemon MEMORY and are re-converged on
36421
- * reconnect; hand-authored agents keep tokens in their own `agent.json` (the
36422
- * same trust boundary as the daemon process).
36423
- *
36424
- * `signingSecret` is intentionally absent: Socket Mode authenticates with the
36425
- * app-level token, so the daemon never needs a signing secret.
36426
- */
36427
- /** Trigger match — mirrors the daemon BindRuleConfig.match (agents/agent-schema.ts). */
36428
- const BindMatch = discriminatedUnion("kind", [
36429
- object({ kind: literal("mention") }),
36430
- object({ kind: literal("dm") }),
36431
- object({
36432
- kind: literal("keyword"),
36433
- value: string()
36434
- }),
36435
- object({ kind: literal("auto") })
36436
- ]);
36437
- /** One channel/thread trigger binding — mirrors the daemon BindRuleConfig. */
36438
- const IntegrationBindRule = object({
36439
- channel: string().optional(),
36440
- thread: string().optional(),
36441
- match: BindMatch
36442
- });
36443
- object({
36444
- botToken: string(),
36445
- appToken: string().optional(),
36446
- appId: string().optional(),
36447
- shareable: boolean().default(false),
36448
- botUserId: string().optional()
36449
- });
36450
- object({ botToken: string() });
36451
- object({
36452
- botToken: string(),
36453
- applicationId: string().optional()
36454
- });
36455
- /**
36456
- * The Feishu / Lark config payload — the long-connection WebSocket
36457
- * (`@larksuiteoapi/node-sdk` `WSClient`) + REST client. A Feishu self-built app
36458
- * authenticates with an `appId` + `appSecret` PAIR — the SDK exchanges them for a
36459
- * short-lived `tenant_access_token` internally (no Slack-style app-level token, no
36460
- * signing secret). `appId` is a semi-public identifier (`cli_…`); `appSecret` is
36461
- * plaintext secret material — NEVER log it. `botOpenId` is the bot's own open_id
36462
- * for @-mention routing; lazily resolved by the daemon via `bot/info` if absent.
36463
- *
36464
- * The envelope's `mode` decides the transport: `direct` opens the SDK long
36465
- * connection on the daemon; `shared` keeps only the authenticated REST client
36466
- * (callbacks arrive through the relay, pre-addressed over rd/*).
36467
- *
36468
- * `region` selects the open-platform gateway the daemon SDK (and CP verifier)
36469
- * talk to — `'feishu'` = mainland China (`open.feishu.cn`, the SDK default) vs
36470
- * `'lark'` = international (`open.larksuite.com`). Same app model, different host;
36471
- * an app is registered in exactly one region. Defaults to `'feishu'` so existing
36472
- * installs are unaffected.
36473
- */
36474
- const FeishuRegion = _enum(["feishu", "lark"]);
36475
- object({
36476
- appId: string(),
36477
- appSecret: string(),
36478
- botOpenId: string().optional(),
36479
- region: FeishuRegion.default("feishu")
36480
- });
36481
- /**
36482
- * §6.4 core routing ENVELOPE (integration-plugin-architecture.md D4): the knobs CORE
36483
- * reads — routing, gating, ingress mode — platform-independent. This is the ONLY
36484
- * carrier of these knobs on the wire: the opaque per-platform `config` payload
36485
- * never duplicates them (the daemon reads routing exclusively from here).
36486
- */
36487
- const IntegrationCoreEnvelope = object({
36488
- mode: _enum(["direct", "shared"]).default("direct"),
36489
- bindRules: array(IntegrationBindRule).default([]),
36490
- mutedChannels: array(string()).default([]),
36491
- gated: boolean().default(false)
36492
- });
36493
- object({
36494
- integrationId: string().uuid(),
36495
- agentId: string().uuid(),
36496
- platform: Platform,
36497
- core: IntegrationCoreEnvelope,
36498
- config: unknown().optional()
36499
- });
36500
- object({ integrationId: string().uuid() });
36501
- /**
36502
- * One conversation the bot participates in (metadata only — no messages).
36503
- * `kind` distinguishes member channels from direct conversations (resource-
36504
- * visibility.md §14.3): absent = 'channel' for wire compatibility. DM rows
36505
- * (`kind: 'im'`, Slack "D…" ids) are reported for every integration on first
36506
- * inbound DM; their `name` is the counterpart's display name. Group DMs
36507
- * (`kind: 'mpim'`, Slack multi-person DMs) are reported on observation the same
36508
- * way — never enumerated, because Slack does not list them as bot membership —
36509
- * but they behave like a channel: several humans share the room, so the agent
36510
- * stays mention-gated there rather than answering every message.
36511
- *
36512
- * `spaceId`/`space` identify the container the conversation lives in — a Discord
36513
- * GUILD, which a bot in several servers needs for the channel to be identifiable at
36514
- * all (every server has a "#general"). The ID is the identity: two distinct guilds
36515
- * may carry the SAME name, so grouping on the name alone would merge them and hide
36516
- * the ambiguity it was meant to resolve. `space` is the display label only. Both are
36517
- * absent on platforms with one implicit container per bot (Slack workspace, Telegram,
36518
- * Feishu tenant) and on DM rows.
36519
- */
36520
- const IntegrationChannel = object({
36521
- id: string(),
36522
- name: string().optional(),
36523
- spaceId: string().optional(),
36524
- space: string().optional(),
36525
- isPrivate: boolean().optional(),
36526
- kind: _enum([
36527
- "channel",
36528
- "im",
36529
- "mpim"
36530
- ]).optional()
36531
- });
36532
- object({
36533
- integrationId: string().uuid(),
36534
- channels: array(IntegrationChannel),
36535
- authoritative: boolean().optional(),
36536
- removed: array(string()).optional()
36537
- });
36538
- /**
36539
- * What a leave targets. Platforms disagree about what a bot can withdraw from, and
36540
- * the difference is not cosmetic — so the caller has to say which it means rather
36541
- * than the daemon guessing from an id:
36542
- *
36543
- * - `conversation` — one channel/group the bot is a member of (Slack
36544
- * `conversations.leave`, Telegram `leaveChat`).
36545
- * - `space` — the whole container. Discord has no per-channel membership for a
36546
- * bot at all: it is in a GUILD and sees that guild's channels through
36547
- * permissions, so the only thing it can leave is the entire server. That is a
36548
- * much larger action than leaving one channel and must be requested as such.
36549
- */
36550
- const IntegrationLeaveTarget = discriminatedUnion("kind", [object({
36551
- kind: literal("conversation"),
36552
- channel: string().min(1)
36553
- }), object({
36554
- kind: literal("space"),
36555
- spaceId: string().min(1)
36556
- })]);
36557
- object({
36558
- integrationId: string().uuid(),
36559
- target: IntegrationLeaveTarget
36560
- });
36561
- object({
36562
- integrationId: string().uuid(),
36563
- channels: array(string()).min(1)
36564
- });
36565
- object({
36566
- ok: boolean(),
36567
- error: string().optional()
36568
- });
36569
- //#endregion
36570
- //#region ../protocol/dist/frames/cron.js
36571
- /**
36572
- * Cron sinks to the daemon (D5) — protocol §5.4.
36573
- *
36574
- * A cron periodically triggers ONE AGENT with a synthetic prompt (`trigger`) to
36575
- * carry out some work. The CP owns the definition; the daemon owns firing +
36576
- * last-run persistence, so crons fire even when the CP is down. On receipt the
36577
- * daemon persists the def into the owning agent's `agent.json` `crons[]` (the
36578
- * single source of truth, same model as integrations) — surviving a restart
36579
- * with the CP down.
36580
- *
36581
- * `target` is OPTIONAL output routing: when present, the daemon posts the
36582
- * trigger as a real message in that channel and the agent's session replies in
36583
- * its thread; when absent the fire is headless (the agent works with no
36584
- * platform output).
36585
- */
36586
- const CronTarget = object({
36587
- platform: Platform.default("slack"),
36588
- channel: string(),
36589
- integrationId: string().uuid().optional()
36590
- });
36591
- object({
36592
- cronId: string().uuid(),
36593
- agentId: string().uuid(),
36594
- schedule: string(),
36595
- timezone: string().min(1),
36596
- target: CronTarget.optional(),
36597
- trigger: string(),
36598
- enabled: boolean().default(true)
36599
- });
36600
- object({ cronId: string().uuid() });
36601
- /**
36602
- * `cron/report` (D→C EVT, fire-and-forget) — one CP-owned cron fired. The
36603
- * daemon stamps the fire into its local store first (it stays authoritative,
36604
- * §5.4) and reports it here so the console's `lastRunAt` converges; the CP
36605
- * upsert is latest-wins, so the daemon re-asserting its stored stamps on
36606
- * reconnect (fires while the CP was unreachable) is idempotent and can never
36607
- * regress the value. Hand-authored (no-origin) crons are never reported.
36608
- *
36609
- * Reports are keyed by `(cronId, firedAt)`: the FIRE report (no `status`)
36610
- * opens the run, an optional SESSION report attaches the ACP session as soon
36611
- * as it is initialized, and the COMPLETION report (with `status` + outcome
36612
- * fields) closes the run once the dispatched turn ends. A completion without
36613
- * a prior fire report (CP was down at fire time) still creates the run row.
36614
- */
36615
- const CronRunStatus = _enum(["success", "failed"]);
36616
- object({
36617
- cronId: string().uuid(),
36618
- agentId: string().uuid(),
36619
- firedAt: string().datetime(),
36620
- status: CronRunStatus.optional(),
36621
- durationMs: number$1().int().nonnegative().optional(),
36622
- sessionId: string().optional(),
36623
- reason: string().optional()
36624
- });
36625
- object({ cronId: string().uuid() });
36626
- //#endregion
36627
- //#region ../protocol/dist/frames/collab.js
36628
- /**
36629
- * Bot-AGNOSTIC agent-collaboration routing snapshot (agent-collaboration §2.3 / §6.2 / §6.5).
36630
- *
36631
- * The existing shared-bot `members` table is keyed by botId (`BotAssignment`) and
36632
- * CANNOT address an agent on a DIFFERENT bot / arbitrary channel. This snapshot is
36633
- * the fix: it maps a channel — `(orgId, platform, channelId)` — to the per-agent
36634
- * placement + call policy the relay needs to route a cross-daemon `rd/agentmsg` and
36635
- * the target daemon needs to terminal-verify a remote caller.
36636
- *
36637
- * It carries NO message body — pure routing/policy metadata, like the rest of the
36638
- * control plane. The SAME shape is distributed two ways (§6.5):
36639
- * - CP→relay over the `rc/*` wire (`rc/collab-routes`) — the relay routes
36640
- * `toAgentId` → owning `daemonId` and authorizes the caller/target policy.
36641
- * - CP→daemon over the daemon↔CP wire — as a `register/ok` field (reconnect
36642
- * baseline) + a `collaboration/routes` EVT (hot push) — so the OWNING daemon of
36643
- * the target can terminal-verify (defense in depth, §2.5 #4) the remote caller's
36644
- * org/channel/placement against its OWN copy, never trusting the relay's claim
36645
- * blindly.
36646
- *
36647
- * FOLLOW-UP (scoped down in P2, see PR description): the full versioned lifecycle
36648
- * of §6.5 — per-entry tombstones, TTL/expiry after a CP disconnect, and
36649
- * fail-closed-on-stale — is NOT fully implemented here. `generation` is present as
36650
- * the version hook and the snapshot is FULL-REPLACE (converge-don't-diff, same as
36651
- * `register/ok`), which is enough to route + authorize on a live CP. TTL-expiry and
36652
- * tombstone semantics are a follow-up within this phase.
36653
- */
36654
- /** One agent's placement + call policy within a channel. `daemonId` is the owning
36655
- * daemon the relay forwards to; `integrationId` is the DEFINITE reply integration
36656
- * (§6.2 — no fallback to "first connection"). */
36657
- const CollabAgentPlacement = object({
36658
- agentId: string().uuid(),
36659
- daemonId: string().uuid(),
36660
- integrationId: string().uuid().optional(),
36661
- /** Public Slack app id (`A…`) for this agent's bot. Receivers use it to recognize
36662
- * AgentConnect-authored platform messages — the first half of verifying an
36663
- * authorship claim (send-message-routing-rework.md §4). */
36664
- botAppId: string().optional(),
36665
- /** Public platform member id this agent's bot posts as (Slack `U…`) — the id a
36666
- * `<@…>` mention resolves to. With {@link botShared} and `name` it is the complete
36667
- * input for the conversation-specific mention ADDRESS
36668
- * (send-message-routing-rework.md §8.5), which both directions need: rendering an
36669
- * exact address to post, and resolving an inbound mention back to an agent.
36670
- * Public metadata, never a credential. */
36671
- botUserId: string().optional(),
36672
- /** True when {@link botUserId} backs MORE THAN ONE agent (a shared bot). The bot user
36673
- * id alone then cannot identify an agent, so its address carries the agent slug too
36674
- * (`<@U_SHARED> reviewer`) and a BARE mention of it addresses no specific agent —
36675
- * which is why a shared bot with no exact author claim fails closed at ingress (§4)
36676
- * and why a bare shared-bot mention from an agent must not fall back to the channel
36677
- * default (§6). */
36678
- botShared: boolean().optional(),
36679
- callPolicy: _enum(["all", "selected"]).default("all"),
36680
- allowedCallerAgentIds: array(string()).default([]),
36681
- /** Caller-side authorization. Effective A→B access requires A's outbound
36682
- * policy to admit B and B's inbound call policy to admit A. */
36683
- outboundPolicy: _enum(["all", "selected"]).default("all"),
36684
- allowedTargetAgentIds: array(string()).default([]),
36685
- name: string().optional(),
36686
- displayName: string().optional()
36687
- });
36688
- /**
36689
- * One agent's placement + call policy carried OUTSIDE any channel — the org-scoped
36690
- * peer directory entry.
36691
- *
36692
- * Every structure on the CP→daemon and CP→relay wires is channel-keyed, so an agent
36693
- * with NO IM integration (webchat, hook, dream, memory-only) never appears in any
36694
- * `channels[]` entry at all. The channel-keyed snapshot structurally cannot express
36695
- * "which agents exist in this org", which is precisely the input channel-free
36696
- * authorization needs: discovery and A2A authorization depend only on the directional
36697
- * call policy (`outboundPolicy`/`allowedTargetAgentIds` on the caller,
36698
- * `callPolicy`/`allowedCallerAgentIds` on the target), org-scoped, with channel
36699
- * demoted to an optional filter. Hence the flat list below.
36700
- */
36701
- const CollabOrgAgent = CollabAgentPlacement.extend({ orgId: string().min(1) });
36702
- /** All agents present in one channel, across daemons. `orgId` scopes routing +
36703
- * authorization: a cross-org caller/target pair never resolves (§2.5 — cross-org
36704
- * rejected). */
36705
- const CollabChannelRoute = object({
36706
- orgId: string().min(1),
36707
- platform: Platform,
36708
- channelId: string().min(1),
36709
- agents: array(CollabAgentPlacement)
36710
- });
36711
- /**
36712
- * The full collaboration snapshot — FULL-REPLACE (converge-don't-diff): the
36713
- * recipient replaces its whole table with `channels`. `generation` monotonically
36714
- * increases per source so a recipient can ignore a stale re-order (version hook for
36715
- * the §6.5 lifecycle follow-up).
36716
- */
36717
- const CollabRoutesSnapshot = object({
36718
- generation: number$1().int().nonnegative().default(0),
36719
- channels: array(CollabChannelRoute).default([]),
36720
- /**
36721
- * FLAT org-scoped directory, alongside (not instead of) `channels`. It is the only
36722
- * place an integration-less agent can appear — see `CollabOrgAgent` — and therefore
36723
- * the authorization input for channel-free A2A. `default([])` keeps a snapshot from
36724
- * an older CP (which advertises no `agent-directory-org-scope-v1`) decodable.
36725
- */
36726
- agents: array(CollabOrgAgent).default([]),
36727
- /**
36728
- * §6.1 origin-kind classification for the platform ids the EMITTER knows — how an
36729
- * older peer classifies an id a newer CP introduces (its own built-in seed only
36730
- * covers ids its build shipped with). Consumers overlay these entries on their
36731
- * seed; an id classified by neither defaults to `'chat'`, the fail-closed answer
36732
- * wherever placements matter (`coordsDecision`). Both fields read as open strings
36733
- * per the S1a rule. `default([])` keeps a pre-S1b snapshot decodable.
36734
- */
36735
- platformKinds: array(object({
36736
- platformId: string().min(1),
36737
- originKind: string().min(1)
36738
- })).default([])
36739
- });
36740
- //#endregion
36741
- //#region ../protocol/dist/frames/hook.js
36742
- /** Decimal wire form for Prisma/GitHub bigint values. */
36743
- const HookBigIntString = string().regex(/^(?:0|[1-9]\d*)$/);
36744
- /** The highest formal-review event one hook permits. */
36745
- const HookReviewPolicy = _enum([
36746
- "off",
36747
- "comment",
36748
- "request_changes",
36749
- "full"
36750
- ]);
36751
- /** External run-reporting transport. `status` is reserved for the later R3. */
36752
- const HookReportingMode = _enum([
36753
- "off",
36754
- "check",
36755
- "status"
36756
- ]);
36757
- /** R2a only admits informational; `required` remains a fail-closed future mode. */
36758
- const HookGateMode = _enum(["informational", "required"]);
36759
- /**
36760
- * Exact config + dispatch identity captured when the CP compiles a hook fire.
36761
- *
36762
- * The relay-facing frames expose these fields individually at top level so an
36763
- * older CP/relay can omit them without making the whole fire undecodable. A
36764
- * consumer MUST have every field before enabling review/reporting; absence is
36765
- * not filled with a revision and therefore fails closed. Action-time RPCs use
36766
- * this complete object so a partially rolled-out tuple cannot authorize an
36767
- * effect.
36768
- */
36769
- const HookConfigSnapshot = object({
36770
- configRevision: HookBigIntString,
36771
- dispatchRevision: HookBigIntString,
36772
- dispatchDaemonId: string().uuid(),
36773
- reviewPolicy: HookReviewPolicy,
36774
- reportingMode: HookReportingMode,
36775
- gateMode: HookGateMode
36776
- });
36777
- /** Optional top-level form used only on rolling-compatible delivery frames. */
36778
- const OptionalHookConfigSnapshot = HookConfigSnapshot.partial();
36779
- /**
36780
- * Trusted, body-free GitHub subject/revision metadata. It is deliberately
36781
- * separate from HookContext: HookContext contains attacker-authored prompt
36782
- * excerpts, whereas this object is cut from a signature-verified payload and
36783
- * fenced to the CP-compiled numeric repo/installation rule.
36784
- *
36785
- * `headSha` is absent for PR issue_comment deliveries because GitHub omits the
36786
- * revision there; the daemon resolves it before `hook/start`. R2a informational
36787
- * checks use `reportSha=headSha` once known. Review-comment deliveries may also
36788
- * identify the triggering comment and its normalized thread root; both remain
36789
- * optional so mixed-version relay/daemon rollouts continue to decode.
36790
- */
36791
- const GithubHookMetadata = object({
36792
- repoId: HookBigIntString,
36793
- repoFullName: string().min(1),
36794
- sourceInstallationId: HookBigIntString,
36795
- subjectKind: _enum(["issue", "pull_request"]),
36796
- pullNumber: number$1().int().positive().optional(),
36797
- headSha: string().min(1).optional(),
36798
- baseSha: string().min(1).optional(),
36799
- reportSha: string().min(1).optional(),
36800
- headRepoFullName: string().min(1).optional(),
36801
- mergeCommitSha: string().min(1).optional(),
36802
- isDraft: boolean().optional(),
36803
- baseChanged: boolean().optional(),
36804
- explicitReviewRequest: boolean().optional(),
36805
- reviewCommentId: HookBigIntString.optional(),
36806
- reviewThreadRootCommentId: HookBigIntString.optional()
36807
- }).superRefine((value, ctx) => {
36808
- if (value.subjectKind === "pull_request" && value.pullNumber === void 0) ctx.addIssue({
36809
- code: "custom",
36810
- path: ["pullNumber"],
36811
- message: "pullNumber is required for a pull_request subject"
36812
- });
36813
- if (value.reportSha !== void 0 && value.headSha === void 0) ctx.addIssue({
36814
- code: "custom",
36815
- path: ["reportSha"],
36816
- message: "reportSha requires an authoritative headSha"
36817
- });
36818
- if (value.reportSha !== void 0 && value.headSha !== void 0 && value.reportSha !== value.headSha) ctx.addIssue({
36819
- code: "custom",
36820
- path: ["reportSha"],
36821
- message: "R2a reports only the authoritative pull head SHA"
36822
- });
36823
- });
36824
- const HookReviewEvent = _enum([
36825
- "COMMENT",
36826
- "REQUEST_CHANGES",
36827
- "APPROVE"
36828
- ]);
36829
- const HookReviewVerdict = _enum([
36830
- "pass",
36831
- "fail",
36832
- "neutral"
36833
- ]);
36834
- /** Short machine code only; raw GitHub/runtime exception text never crosses to reporting. */
36835
- const HookReviewResultCode = string().min(1).max(100).regex(/^[a-z0-9_:-]+$/);
36836
- /**
36837
- * Body-free outcome of one reserved formal-review attempt.
36838
- *
36839
- * `not_submitted` is only legal after a deterministic no-effect failure and
36840
- * lets the CP release the reservation. An uncertain POST remains `ambiguous`
36841
- * until marker reconciliation proves whether GitHub created it.
36842
- */
36843
- const HookReviewResult = discriminatedUnion("state", [
36844
- object({
36845
- state: literal("submitted"),
36846
- reviewId: string().min(1),
36847
- event: HookReviewEvent,
36848
- verdict: HookReviewVerdict,
36849
- commitId: string().min(1)
36850
- }),
36851
- object({
36852
- state: literal("not_submitted"),
36853
- code: HookReviewResultCode
36854
- }),
36855
- object({
36856
- state: literal("ambiguous"),
36857
- code: HookReviewResultCode
36858
- })
36859
- ]);
36860
- /** Body-free identity of the daemon's one fallback GitHub comment. */
36861
- const GithubPublishedComment = object({
36862
- kind: _enum(["issue_comment", "review_comment"]),
36863
- commentId: HookBigIntString
36864
- });
36865
- object({
36866
- source: _enum(["webhook", "github"]),
36867
- event: string().optional(),
36868
- action: string().optional(),
36869
- repo: string().optional(),
36870
- number: number$1().int().optional(),
36871
- title: string().optional(),
36872
- senderLogin: string().optional(),
36873
- senderAvatarUrl: string().url().optional(),
36874
- authorAssociation: string().optional(),
36875
- labels: array(string()).optional(),
36876
- htmlUrl: string().optional(),
36877
- bodyExcerpt: string().optional(),
36878
- body: string().optional(),
36879
- truncated: boolean().optional()
36880
- });
36881
- object({
36882
- hookId: string().uuid(),
36883
- agentId: string().uuid(),
36884
- deliveryKey: string().min(1),
36885
- ...OptionalHookConfigSnapshot.shape,
36886
- event: string().min(1).optional(),
36887
- github: GithubHookMetadata.optional(),
36888
- status: _enum(["success", "failed"]),
36889
- durationMs: number$1().int().nonnegative().optional(),
36890
- sessionId: string().optional(),
36891
- reason: string().optional(),
36892
- reviewAttemptId: string().uuid().optional(),
36893
- reviewResult: HookReviewResult.optional(),
36894
- publishedComment: GithubPublishedComment.optional()
36895
- }).superRefine((report, ctx) => {
36896
- if (report.reviewAttemptId === void 0 !== (report.reviewResult === void 0)) ctx.addIssue({
36897
- code: "custom",
36898
- path: report.reviewAttemptId === void 0 ? ["reviewAttemptId"] : ["reviewResult"],
36899
- message: "reviewAttemptId and reviewResult must be reported together"
36900
- });
36901
- if (report.publishedComment && report.reviewResult?.state === "submitted") ctx.addIssue({
36902
- code: "custom",
36903
- path: ["publishedComment"],
36904
- message: "a submitted formal review and fallback comment are mutually exclusive"
36905
- });
36906
- });
36907
- object({
36908
- hookId: string().uuid(),
36909
- agentId: string().uuid(),
36910
- deliveryKey: string().min(1),
36911
- sessionId: string().min(1).optional(),
36912
- event: string().min(1).optional(),
36913
- github: GithubHookMetadata,
36914
- ...HookConfigSnapshot.shape
36915
- });
36916
- object({ accepted: literal(true) });
36917
- object({
36918
- hookId: string().uuid(),
36919
- deliveryKey: string().min(1),
36920
- attemptId: string().uuid(),
36921
- requestedEvent: HookReviewEvent,
36922
- requestedVerdict: HookReviewVerdict,
36923
- snapshot: HookConfigSnapshot
36924
- });
36925
- object({
36926
- attemptId: string().uuid(),
36927
- token: string().min(1),
36928
- ttlSec: number$1().int().positive(),
36929
- expiresAt: string().datetime(),
36930
- repoId: HookBigIntString,
36931
- repoFullName: string().min(1),
36932
- pullNumber: number$1().int().positive(),
36933
- expectedHeadSha: string().min(1),
36934
- expectedBaseSha: string().min(1)
36935
- });
36936
- object({
36937
- hookId: string().uuid(),
36938
- deliveryKey: string().min(1),
36939
- attemptId: string().uuid(),
36940
- snapshot: HookConfigSnapshot,
36941
- result: HookReviewResult
36942
- });
36943
- object({ accepted: literal(true) });
36944
- //#endregion
36945
- //#region ../protocol/dist/frames/remote-mcp.js
36946
- /** Non-secret entitlement carried CP → relay → daemon after webchat verification. */
36947
- const WebchatRemoteMcpEntitlement = object({
36948
- authorityId: string().uuid(),
36949
- authorityGeneration: number$1().int().positive(),
36950
- expiresAt: string().datetime()
36951
- }).strict();
36952
- const DescriptorFence = object({
36953
- authorityId: string().uuid(),
36954
- authorityGeneration: number$1().int().positive(),
36955
- conversationId: string().uuid(),
36956
- descriptorInstanceId: string().uuid(),
36957
- grantRevision: number$1().int().positive()
36958
- }).strict();
36959
- object({
36960
- authorityId: string().uuid(),
36961
- authorityGeneration: number$1().int().positive(),
36962
- conversationId: string().uuid(),
36963
- descriptorInstanceId: string().uuid()
36964
- }).strict();
36965
- DescriptorFence.extend({
36966
- grantId: string().uuid(),
36967
- token: string().min(32),
36968
- expiresAt: string().datetime(),
36969
- mcpUrl: string().url()
36970
- }).strict();
36971
- DescriptorFence.extend({ grantId: string().uuid() }).strict().extend({ activated: boolean() }).strict();
36972
- object({
36973
- authorityId: string().uuid(),
36974
- authorityGeneration: number$1().int().positive(),
36975
- conversationId: string().uuid(),
36976
- reason: _enum([
36977
- "session_closed",
36978
- "session_expired",
36979
- "agent_detached",
36980
- "feature_disabled",
36981
- "security"
36982
- ])
36983
- }).strict().pick({
36984
- authorityId: true,
36985
- authorityGeneration: true,
36986
- conversationId: true
36987
- }).extend({ revoked: boolean() }).strict();
36988
- //#endregion
36989
- //#region ../protocol/dist/frames/relay-cp.js
36990
- const RcAuth = object({
36991
- method: _enum(["token", "apikey"]),
36992
- credential: string().min(1)
36993
- });
36994
- const RcDeploymentConfig = object({
36995
- revision: number$1().int().nonnegative(),
36996
- githubWebhookSecret: string().min(1).optional()
36997
- });
36998
- const RcAuthOk = object({
36999
- heartbeatSec: number$1().int(),
37000
- serverTime: string().datetime(),
37001
- deploymentConfig: RcDeploymentConfig.optional()
37002
- });
37003
- const RcRegister = object({
37004
- name: string().min(1),
37005
- daemonUrl: string().min(1)
37006
- });
37007
- const RcRegistered = object({ relayId: string().uuid() });
37008
- const RcHeartbeat = object({});
37009
- const RcVerify = discriminatedUnion("kind", [object({
37010
- kind: literal("daemon-key"),
37011
- credential: string().min(1)
37012
- }), object({
37013
- kind: literal("webchat-token"),
37014
- credential: string().min(1),
37015
- conversationBinding: literal("v1").optional()
37016
- })]);
37017
- const RcWebchatParticipant = object({
37018
- agentId: string().uuid(),
37019
- daemonId: string().uuid().optional(),
37020
- primary: boolean().optional()
37021
- });
37022
- const RcVerifyResult = object({
37023
- ok: boolean(),
37024
- reason: string().optional(),
37025
- orgId: string().optional(),
37026
- daemonId: string().uuid().optional(),
37027
- userId: string().optional(),
37028
- user: string().optional(),
37029
- agentId: string().uuid().optional(),
37030
- conversationId: string().uuid().optional(),
37031
- participants: array(RcWebchatParticipant).max(16).optional(),
37032
- remoteMcp: WebchatRemoteMcpEntitlement.optional()
37033
- });
37034
- const RcGithubHookFence = object({
37035
- hookId: string().uuid(),
37036
- configRevision: HookBigIntString,
37037
- dispatchRevision: HookBigIntString
37038
- }).strict();
37039
- const RcGithubCommentAuthz = object({
37040
- hookId: string().uuid(),
37041
- installationId: string().regex(/^[1-9]\d*$/),
37042
- repoId: string().regex(/^[1-9]\d*$/),
37043
- repoFullName: string().regex(/^[^/\s]+\/[^/\s]+$/),
37044
- senderLogin: string().min(1),
37045
- subjectAuthorLogin: string().min(1).optional(),
37046
- configRevision: HookBigIntString,
37047
- dispatchRevision: HookBigIntString,
37048
- siblingFences: array(RcGithubHookFence).min(1).optional()
37049
- }).strict();
37050
- const RcGithubCommentAuthzResult = object({ allowed: boolean() }).strict();
37051
- const GithubNumericId = string().regex(/^[1-9]\d*$/);
37052
- const RcGithubRerequest = union([object({
37053
- checkRunId: GithubNumericId,
37054
- repoId: GithubNumericId,
37055
- headSha: string().min(1),
37056
- deliveryKey: string().min(1).max(200),
37057
- includeBaseSha: literal(true).optional()
37058
- }).strict(), object({
37059
- scope: literal("suite"),
37060
- appId: GithubNumericId,
37061
- installationId: GithubNumericId,
37062
- repoId: GithubNumericId,
37063
- headSha: string().min(1),
37064
- deliveryKey: string().min(1).max(200)
37065
- }).strict()]);
37066
- const RcGithubRerequestTarget = object({
37067
- hookId: string().uuid(),
37068
- pullNumber: number$1().int().positive(),
37069
- baseSha: string().min(1),
37070
- configRevision: HookBigIntString,
37071
- dispatchRevision: HookBigIntString
37072
- }).strict();
37073
- const RcGithubRerequestResult = union([
37074
- object({ allowed: literal(false) }).strict(),
37075
- object({
37076
- allowed: literal(true),
37077
- hookId: string().uuid(),
37078
- pullNumber: number$1().int().positive(),
37079
- baseSha: string().min(1).optional(),
37080
- configRevision: HookBigIntString,
37081
- dispatchRevision: HookBigIntString
37082
- }).strict(),
37083
- object({
37084
- allowed: literal(true),
37085
- targets: array(RcGithubRerequestTarget).min(1)
37086
- }).strict()
37087
- ]);
37088
- const RcHookAssign = object({
37089
- hookId: string().uuid(),
37090
- kind: _enum(["webhook", "github"]),
37091
- agentId: string().uuid(),
37092
- daemonId: string().uuid(),
37093
- ...OptionalHookConfigSnapshot.shape,
37094
- sessionMode: _enum([
37095
- "perDelivery",
37096
- "perThread",
37097
- "shared"
37098
- ]),
37099
- target: CronTarget.optional(),
37100
- webhook: object({
37101
- urlToken: string().min(1),
37102
- hmacSecret: string().optional()
37103
- }).optional(),
37104
- github: object({
37105
- repoId: string(),
37106
- repoFullName: string(),
37107
- sessionKeyPrefix: string().min(1).optional(),
37108
- events: array(string()),
37109
- labelFilter: array(string()),
37110
- commentFamilies: array(_enum(["issues", "pull_request"])).optional(),
37111
- mentionOnly: boolean(),
37112
- appSlug: string().optional(),
37113
- agentName: string().optional(),
37114
- installationIds: array(string())
37115
- }).optional()
37116
- }).superRefine((rule, ctx) => {
37117
- if (rule.dispatchDaemonId !== void 0 && rule.dispatchDaemonId !== rule.daemonId) ctx.addIssue({
37118
- code: "custom",
37119
- path: ["dispatchDaemonId"],
37120
- message: "dispatchDaemonId must equal daemonId on a compiled rule"
37121
- });
37122
- });
37123
- const RcHookRemove = object({ hookId: string().uuid() });
37124
- const RcRunReport = object({
37125
- hookId: string().uuid(),
37126
- deliveryKey: string().min(1),
37127
- firedAt: string().datetime(),
37128
- agentId: string().uuid(),
37129
- daemonId: string().uuid().optional(),
37130
- ...OptionalHookConfigSnapshot.shape,
37131
- event: string().min(1).optional(),
37132
- github: GithubHookMetadata.optional(),
37133
- status: _enum(["accepted", "failed"]),
37134
- reason: string().optional()
37135
- }).superRefine((report, ctx) => {
37136
- if (report.daemonId !== void 0 && report.dispatchDaemonId !== void 0 && report.daemonId !== report.dispatchDaemonId) ctx.addIssue({
37137
- code: "custom",
37138
- path: ["dispatchDaemonId"],
37139
- message: "dispatchDaemonId must equal daemonId on a delivery report"
37140
- });
37141
- });
37142
- const RcGithubInstallation = object({
37143
- installationId: string().min(1),
37144
- action: string().min(1)
37145
- });
37146
- const RcDaemonRevoke = object({ daemonId: string().uuid() });
37147
36284
  /** App-level Slack message shortcut for opening the controls of the session that
37148
36285
  * owns the selected message's conversation. Direct apps receive it over Socket
37149
- * Mode; shared apps receive the same callback through the relay HTTP edge. */
36286
+ * Mode; shared apps receive the same callback through the relay HTTP edge.
36287
+ *
36288
+ * Declared to Slack by {@link buildSlackAppManifest} below, which is why it lives
36289
+ * here rather than beside the runtime Block Kit action ids in `frames/relay-cp.ts`:
36290
+ * those name controls on messages we post, this one names a manifest feature. */
37150
36291
  const SLACK_MANAGE_SESSION_SHORTCUT_CALLBACK_ID = "ac_manage_session";
37151
- /** One short choice from the compact Slack status overflow. Slack caps option
37152
- * values at 150 characters; the longer session target rides in `block_id`. */
37153
- const SlackStatusOverflowAction = _enum([
37154
- "switch-agent",
37155
- "manage",
37156
- "cancel"
37157
- ]);
37158
- object({
37159
- v: literal(1),
37160
- action: SlackStatusOverflowAction,
37161
- target: string().min(1)
37162
- }).strict();
37163
- object({
37164
- v: literal(1),
37165
- agentId: string().min(1),
37166
- integrationId: string().min(1),
37167
- sessionKey: string().min(1)
37168
- }).strict();
37169
- const AttributedRoute = object({
37170
- agentId: string().uuid(),
37171
- daemonId: string().uuid(),
37172
- integrationId: string().uuid(),
37173
- scope: object({
37174
- channel: string().optional(),
37175
- thread: string().optional()
37176
- }).optional(),
37177
- match: BindMatch
37178
- });
37179
- const RcBotSecrets = union([
37180
- object({
37181
- botToken: string(),
37182
- signingSecret: string()
37183
- }).catchall(unknown()),
37184
- object({
37185
- verificationToken: string(),
37186
- encryptKey: string().optional()
37187
- }).catchall(unknown()),
37188
- record$1(string(), unknown())
37189
- ]);
37190
- const RcAgentDirEntry = object({
37191
- agentId: string().uuid(),
37192
- name: string(),
37193
- daemonId: string().uuid(),
37194
- integrationId: string().uuid().optional()
37195
- });
37196
- const RcBotAssign = object({
37197
- botId: string().uuid(),
37198
- platform: Platform,
37199
- originKind: string().min(1).optional(),
37200
- credentialRevision: number$1().int().nonnegative().optional(),
37201
- secrets: RcBotSecrets,
37202
- ingress: unknown().optional(),
37203
- members: array(object({
37204
- daemonId: string().uuid(),
37205
- agentIds: array(string().uuid())
37206
- })),
37207
- agents: array(RcAgentDirEntry).default([]),
37208
- routes: array(AttributedRoute),
37209
- defaultAgentId: string().uuid().optional(),
37210
- defaultDaemonId: string().uuid().optional(),
37211
- gatedAgentIds: array(string().uuid()).default([]),
37212
- mutedChannels: array(string()).default([]),
37213
- gatedOffChannels: array(string()).default([]),
37214
- noticeAuthority: string().uuid().optional(),
37215
- noticedDmConversations: array(string()).default([])
37216
- });
37217
- const RcBotUnassign = object({
37218
- botId: string().uuid(),
37219
- credentialRevision: number$1().int().nonnegative().optional()
37220
- });
37221
- const RcRoutes = object({
37222
- botId: string().uuid(),
37223
- members: array(object({
37224
- daemonId: string().uuid(),
37225
- agentIds: array(string().uuid())
37226
- })),
37227
- agents: array(RcAgentDirEntry).default([]),
37228
- routes: array(AttributedRoute),
37229
- defaultAgentId: string().uuid().optional(),
37230
- defaultDaemonId: string().uuid().optional(),
37231
- gatedAgentIds: array(string().uuid()).default([]),
37232
- mutedChannels: array(string()).default([]),
37233
- gatedOffChannels: array(string()).default([]),
37234
- noticeAuthority: string().uuid().optional(),
37235
- noticedDmConversations: array(string()).default([])
37236
- });
37237
- const RcAssign = object({
37238
- botId: string().uuid(),
37239
- sessionKey: string().min(1),
37240
- agentId: string().uuid(),
37241
- daemonId: string().uuid()
37242
- });
37243
- const RcParticipantAssign = object({
37244
- botId: string().uuid(),
37245
- sessionKey: string().min(1),
37246
- agentId: string().uuid(),
37247
- daemonId: string().uuid()
37248
- });
37249
- const RcSetChannelAgent = object({
37250
- botId: string().uuid(),
37251
- channelId: string().min(1),
37252
- agentId: string().uuid()
37253
- });
37254
- const RcBotChannels = object({
37255
- botId: string().uuid(),
37256
- channels: array(IntegrationChannel)
37257
- });
37258
- const RcNoticePosted = object({
37259
- botId: string().uuid(),
37260
- channel: string().min(1)
37261
- });
37262
- const RcBotRevoked = object({
37263
- botId: string().uuid(),
37264
- reason: _enum(["app_uninstalled", "tokens_revoked"]),
37265
- credentialRevision: number$1().int().nonnegative().optional(),
37266
- eventAtMs: number$1().int().nonnegative().optional()
37267
- });
37268
- const RcBotRevokedOk = object({
37269
- botId: string().uuid(),
37270
- applied: boolean()
37271
- });
37272
- /** `type` string → payload schema for the relay↔CP wire. */
37273
- const RELAY_CP_SCHEMAS = {
37274
- "rc/auth": RcAuth,
37275
- "rc/auth/ok": RcAuthOk,
37276
- "rc/register": RcRegister,
37277
- "rc/registered": RcRegistered,
37278
- "rc/heartbeat": RcHeartbeat,
37279
- "rc/verify": RcVerify,
37280
- "rc/verify/ok": RcVerifyResult,
37281
- "rc/github-comment-authz": RcGithubCommentAuthz,
37282
- "rc/github-comment-authz/ok": RcGithubCommentAuthzResult,
37283
- "rc/github-rerequest": RcGithubRerequest,
37284
- "rc/github-rerequest/ok": RcGithubRerequestResult,
37285
- "rc/hook-assign": RcHookAssign,
37286
- "rc/hook-remove": RcHookRemove,
37287
- "rc/run-report": RcRunReport,
37288
- "rc/github-installation": RcGithubInstallation,
37289
- "rc/daemon-revoke": RcDaemonRevoke,
37290
- "rc/bot-assign": RcBotAssign,
37291
- "rc/bot-unassign": RcBotUnassign,
37292
- "rc/routes": RcRoutes,
37293
- "rc/assign": RcAssign,
37294
- "rc/participant-assign": RcParticipantAssign,
37295
- "rc/set-channel-agent": RcSetChannelAgent,
37296
- "rc/bot-channels": RcBotChannels,
37297
- "rc/bot-conversation": object({
37298
- botId: string().uuid(),
37299
- conversation: IntegrationChannel
37300
- }),
37301
- "rc/bot-revoked": RcBotRevoked,
37302
- "rc/bot-revoked/ok": RcBotRevokedOk,
37303
- "rc/notice-posted": RcNoticePosted,
37304
- "rc/thread-assign": object({
37305
- botId: string().uuid(),
37306
- sessionKey: string().min(1),
37307
- agentId: string().uuid(),
37308
- daemonId: string().uuid()
37309
- }),
37310
- "rc/thread-participant": object({
37311
- botId: string().uuid(),
37312
- sessionKey: string().min(1),
37313
- agentId: string().uuid(),
37314
- daemonId: string().uuid()
37315
- }),
37316
- "rc/thread-lookup": object({
37317
- botId: string().uuid(),
37318
- sessionKey: string().min(1)
37319
- }),
37320
- "rc/thread-lookup/ok": object({
37321
- botId: string().uuid(),
37322
- sessionKey: string().min(1),
37323
- target: object({
37324
- agentId: string().uuid(),
37325
- daemonId: string().uuid()
37326
- }).nullable(),
37327
- participants: array(object({
37328
- agentId: string().uuid(),
37329
- daemonId: string().uuid()
37330
- })).default([])
37331
- }),
37332
- "rc/collab-routes": CollabRoutesSnapshot,
37333
- "rc/mcp-assign": object({
37334
- providerId: string().uuid(),
37335
- upstreamUrl: string(),
37336
- headers: array(object({
37337
- name: string(),
37338
- value: string()
37339
- })).default([]),
37340
- grantKeyHashes: array(string().min(1)).min(1)
37341
- }),
37342
- "rc/mcp-unassign": object({
37343
- providerId: string().uuid(),
37344
- grantKeyHash: string().optional()
37345
- }),
37346
- "rc/memoryconnection-assign": object({
37347
- connectionId: string().uuid(),
37348
- revision: number$1().int().positive(),
37349
- upstreamUrl: string().url().max(2048),
37350
- headers: array(object({
37351
- name: string().min(1).max(128),
37352
- value: string().max(16 * 1024)
37353
- })).max(64).default([]),
37354
- grantKeyHashes: array(string().regex(/^[a-f0-9]{64}$/)).min(1).max(64)
37355
- }),
37356
- "rc/memoryconnection-unassign": object({
37357
- connectionId: string().uuid(),
37358
- revision: number$1().int().positive(),
37359
- grantKeyHash: string().regex(/^[a-f0-9]{64}$/).optional()
37360
- }),
37361
- error: ErrorFrame
37362
- };
37363
- Object.keys(RELAY_CP_SCHEMAS);
37364
- discriminatedUnion("type", [
37365
- frameSchema("rc/auth", RELAY_CP_SCHEMAS["rc/auth"]),
37366
- frameSchema("rc/auth/ok", RELAY_CP_SCHEMAS["rc/auth/ok"]),
37367
- frameSchema("rc/register", RELAY_CP_SCHEMAS["rc/register"]),
37368
- frameSchema("rc/registered", RELAY_CP_SCHEMAS["rc/registered"]),
37369
- frameSchema("rc/heartbeat", RELAY_CP_SCHEMAS["rc/heartbeat"]),
37370
- frameSchema("rc/verify", RELAY_CP_SCHEMAS["rc/verify"]),
37371
- frameSchema("rc/verify/ok", RELAY_CP_SCHEMAS["rc/verify/ok"]),
37372
- frameSchema("rc/github-comment-authz", RELAY_CP_SCHEMAS["rc/github-comment-authz"]),
37373
- frameSchema("rc/github-comment-authz/ok", RELAY_CP_SCHEMAS["rc/github-comment-authz/ok"]),
37374
- frameSchema("rc/github-rerequest", RELAY_CP_SCHEMAS["rc/github-rerequest"]),
37375
- frameSchema("rc/github-rerequest/ok", RELAY_CP_SCHEMAS["rc/github-rerequest/ok"]),
37376
- frameSchema("rc/hook-assign", RELAY_CP_SCHEMAS["rc/hook-assign"]),
37377
- frameSchema("rc/hook-remove", RELAY_CP_SCHEMAS["rc/hook-remove"]),
37378
- frameSchema("rc/run-report", RELAY_CP_SCHEMAS["rc/run-report"]),
37379
- frameSchema("rc/github-installation", RELAY_CP_SCHEMAS["rc/github-installation"]),
37380
- frameSchema("rc/daemon-revoke", RELAY_CP_SCHEMAS["rc/daemon-revoke"]),
37381
- frameSchema("rc/bot-assign", RELAY_CP_SCHEMAS["rc/bot-assign"]),
37382
- frameSchema("rc/bot-unassign", RELAY_CP_SCHEMAS["rc/bot-unassign"]),
37383
- frameSchema("rc/routes", RELAY_CP_SCHEMAS["rc/routes"]),
37384
- frameSchema("rc/assign", RELAY_CP_SCHEMAS["rc/assign"]),
37385
- frameSchema("rc/participant-assign", RELAY_CP_SCHEMAS["rc/participant-assign"]),
37386
- frameSchema("rc/set-channel-agent", RELAY_CP_SCHEMAS["rc/set-channel-agent"]),
37387
- frameSchema("rc/bot-channels", RELAY_CP_SCHEMAS["rc/bot-channels"]),
37388
- frameSchema("rc/bot-conversation", RELAY_CP_SCHEMAS["rc/bot-conversation"]),
37389
- frameSchema("rc/bot-revoked", RELAY_CP_SCHEMAS["rc/bot-revoked"]),
37390
- frameSchema("rc/bot-revoked/ok", RELAY_CP_SCHEMAS["rc/bot-revoked/ok"]),
37391
- frameSchema("rc/notice-posted", RELAY_CP_SCHEMAS["rc/notice-posted"]),
37392
- frameSchema("rc/thread-assign", RELAY_CP_SCHEMAS["rc/thread-assign"]),
37393
- frameSchema("rc/thread-participant", RELAY_CP_SCHEMAS["rc/thread-participant"]),
37394
- frameSchema("rc/thread-lookup", RELAY_CP_SCHEMAS["rc/thread-lookup"]),
37395
- frameSchema("rc/thread-lookup/ok", RELAY_CP_SCHEMAS["rc/thread-lookup/ok"]),
37396
- frameSchema("rc/collab-routes", RELAY_CP_SCHEMAS["rc/collab-routes"]),
37397
- frameSchema("rc/mcp-assign", RELAY_CP_SCHEMAS["rc/mcp-assign"]),
37398
- frameSchema("rc/mcp-unassign", RELAY_CP_SCHEMAS["rc/mcp-unassign"]),
37399
- frameSchema("rc/memoryconnection-assign", RELAY_CP_SCHEMAS["rc/memoryconnection-assign"]),
37400
- frameSchema("rc/memoryconnection-unassign", RELAY_CP_SCHEMAS["rc/memoryconnection-unassign"]),
37401
- frameSchema("error", RELAY_CP_SCHEMAS["error"])
37402
- ]);
37403
- //#endregion
37404
- //#region ../protocol/dist/slack-app-manifest.js
37405
36292
  /** Public platform profile copy. Never derive this from an Agent description. */
37406
36293
  const PLATFORM_APP_DESCRIPTION = "AI agent powered by AgentConnect.";
37407
36294
  const SLACK_BOT_SCOPES = [
@@ -57634,6 +56521,26 @@ async function disconnectPrisma() {
57634
56521
  await singleton?.$disconnect();
57635
56522
  singleton = void 0;
57636
56523
  }
56524
+ //#endregion
56525
+ //#region ../control-plane/dist/secrets/scope.js
56526
+ /** Deployment-owned material (the deployment config document's secrets). */
56527
+ const DEPLOYMENT_SCOPE = { kind: "deployment" };
56528
+ /**
56529
+ * Envelope marker for values sealed under a SCOPED key. It carries a format
56530
+ * version and nothing else — deliberately not the tenant id (see above). Its
56531
+ * one job is to let `open` tell a scoped value apart from a pre-scoping one, so
56532
+ * the rollout needs no backfill.
56533
+ */
56534
+ const SECRET_ENVELOPE_PREFIX = "acv1:";
56535
+ /**
56536
+ * Org key names default to `<deployment key>-org-`, so they inherit whatever
56537
+ * namespace the deployment key already occupies. Deployments commonly share one
56538
+ * transit mount and rely on key naming alone to stay separated; a fixed prefix
56539
+ * would collide their org keys, a derived one cannot.
56540
+ */
56541
+ function effectiveOrgKeyPrefix(transitKey, configured) {
56542
+ return configured ?? `${transitKey}-org-`;
56543
+ }
57637
56544
  const HttpUrlSchema = string().url().superRefine((value, ctx) => {
57638
56545
  const url = new URL(value);
57639
56546
  if (url.protocol !== "http:" && url.protocol !== "https:") ctx.addIssue({
@@ -57888,7 +56795,7 @@ var DeploymentConfigService = class {
57888
56795
  for (const [rawKey, value] of Object.entries(secretPatch)) {
57889
56796
  const key = DeploymentSecretKeySchema.parse(rawKey);
57890
56797
  prepared[key] = value === null ? null : {
57891
- sealedValue: await this.cipher.seal(value),
56798
+ sealedValue: await this.cipher.seal(value, DEPLOYMENT_SCOPE),
57892
56799
  fingerprint: fingerprint(value)
57893
56800
  };
57894
56801
  }
@@ -57909,7 +56816,7 @@ var DeploymentConfigService = class {
57909
56816
  const secrets = {};
57910
56817
  for (const secret of row.secrets) {
57911
56818
  const parsed = DeploymentSecretKeySchema.safeParse(secret.key);
57912
- if (parsed.success && openedKeys.has(parsed.data)) secrets[parsed.data] = await this.cipher.open(secret.sealedValue);
56819
+ if (parsed.success && openedKeys.has(parsed.data)) secrets[parsed.data] = await this.cipher.open(secret.sealedValue, DEPLOYMENT_SCOPE);
57913
56820
  }
57914
56821
  return {
57915
56822
  schemaVersion: 1,
@@ -58105,12 +57012,19 @@ var PgDeploymentConfigStore = class extends DeploymentConfigService {
58105
57012
  *
58106
57013
  * Envelope encryption as a service: the data key never leaves Vault; the CP
58107
57014
  * sends base64 plaintext to `transit/encrypt/<key>` and stores the returned
58108
- * self-describing `vault:vN:…` ciphertext in the existing text columns.
57015
+ * ciphertext, behind this deployment's envelope tag, in the existing text
57016
+ * columns. WHICH key is chosen comes from the {@link SecretScope} the caller
57017
+ * passes — the deployment's key, or `<orgKeyPrefix><orgId>` — so an
57018
+ * organization's material can be destroyed with its key
57019
+ * (docs/designs/per-org-secret-encryption.md).
58109
57020
  *
58110
57021
  * Contract (pinned on the port):
58111
- * - `open` PASSES THROUGH values it did not seal (no `vault:vN:` prefix
58112
- * return as-is): existing plaintext rows keep reading after the flip, and the
58113
- * next write re-seals them — the rollout is online, no backfill required.
57022
+ * - `open` PASSES THROUGH values it did not seal (neither the envelope tag nor
57023
+ * a bare `vault:vN:` prefix ⇒ return as-is): existing plaintext rows keep
57024
+ * reading after the flip, and the next write re-seals them — the rollout is
57025
+ * online, no backfill required.
57026
+ * - A value sealed under one scope does NOT open under another: Transit rejects
57027
+ * a ciphertext presented to a different key, which is the cross-tenant fence.
58114
57028
  * - No argument or response body is ever logged; errors carry only the HTTP
58115
57029
  * status and Vault's `errors[]` strings.
58116
57030
  *
@@ -58121,11 +57035,13 @@ var PgDeploymentConfigStore = class extends DeploymentConfigService {
58121
57035
  * that exact login wire shape ({role, jwt}), so nothing here is bound to
58122
57036
  * Kubernetes — a k8s ServiceAccount token is just the common jwtPath.
58123
57037
  *
58124
- * `open` results are cached in-process keyed by ciphertext (bounded, insertion-
58125
- * order eviction): reconcile opens every owned agent's secrets per register, and
58126
- * Transit ciphertexts are stable until re-sealed, so the cache turns that into
58127
- * one network call per distinct value. `seal` is never cached — Transit returns
58128
- * fresh ciphertext per call by design.
57038
+ * `open` results are cached in-process keyed by key name AND ciphertext
57039
+ * (bounded, insertion-order eviction): reconcile opens every owned agent's
57040
+ * secrets per register, and Transit ciphertexts are stable until re-sealed, so
57041
+ * the cache turns that into one network call per distinct value. The key name
57042
+ * belongs in the cache key because the stored string alone no longer implies
57043
+ * one. `seal` is never cached — Transit returns fresh ciphertext per call by
57044
+ * design.
58129
57045
  */
58130
57046
  /** Transit ciphertext is self-describing: `vault:v<key-version>:<base64>`. */
58131
57047
  const CIPHERTEXT_RE = /^vault:v\d+:/;
@@ -58134,6 +57050,7 @@ const RENEW_FRACTION = .8;
58134
57050
  var VaultTransitSecretCipher = class {
58135
57051
  base;
58136
57052
  key;
57053
+ orgKeyPrefix;
58137
57054
  mount;
58138
57055
  namespace;
58139
57056
  auth;
@@ -58146,6 +57063,7 @@ var VaultTransitSecretCipher = class {
58146
57063
  constructor(opts) {
58147
57064
  this.base = `${opts.addr.replace(/\/+$/, "")}/v1`;
58148
57065
  this.key = opts.key;
57066
+ this.orgKeyPrefix = opts.orgKeyPrefix;
58149
57067
  this.mount = opts.mount ?? "transit";
58150
57068
  this.namespace = opts.namespace;
58151
57069
  this.auth = opts.auth;
@@ -58153,19 +57071,31 @@ var VaultTransitSecretCipher = class {
58153
57071
  this.now = opts.now ?? Date.now;
58154
57072
  this.openCacheMax = opts.openCacheMax ?? 5e3;
58155
57073
  }
58156
- async seal(plaintext) {
58157
- const data = await this.transit("encrypt", { plaintext: Buffer.from(plaintext, "utf8").toString("base64") });
58158
- if (typeof data.ciphertext !== "string") throw new Error("vault transit encrypt: no ciphertext in response");
58159
- return data.ciphertext;
57074
+ /**
57075
+ * The key a scope resolves to. Org keys are created lazily by Transit on the
57076
+ * first encrypt (the policy must permit creation on that path); nothing here
57077
+ * is coupled to organization creation.
57078
+ */
57079
+ keyFor(scope) {
57080
+ return scope.kind === "deployment" ? this.key : `${this.orgKeyPrefix}${scope.orgId}`;
58160
57081
  }
58161
- async open(stored) {
58162
- if (!CIPHERTEXT_RE.test(stored)) return stored;
58163
- const hit = this.openCache.get(stored);
57082
+ async seal(plaintext, scope) {
57083
+ const data = await this.transit("encrypt", this.keyFor(scope), { plaintext: Buffer.from(plaintext, "utf8").toString("base64") });
57084
+ if (typeof data.ciphertext !== "string") throw new Error("vault transit encrypt: no ciphertext in response");
57085
+ return `${SECRET_ENVELOPE_PREFIX}${data.ciphertext}`;
57086
+ }
57087
+ async open(stored, scope) {
57088
+ const scoped = stored.startsWith(SECRET_ENVELOPE_PREFIX);
57089
+ if (!scoped && !CIPHERTEXT_RE.test(stored)) return stored;
57090
+ const key = scoped ? this.keyFor(scope) : this.key;
57091
+ const ciphertext = scoped ? stored.slice(5) : stored;
57092
+ const cacheKey = `${key}\u0000${ciphertext}`;
57093
+ const hit = this.openCache.get(cacheKey);
58164
57094
  if (hit !== void 0) return hit;
58165
- const data = await this.transit("decrypt", { ciphertext: stored });
57095
+ const data = await this.transit("decrypt", key, { ciphertext });
58166
57096
  if (typeof data.plaintext !== "string") throw new Error("vault transit decrypt: no plaintext in response");
58167
57097
  const value = Buffer.from(data.plaintext, "base64").toString("utf8");
58168
- this.cacheOpen(stored, value);
57098
+ this.cacheOpen(cacheKey, value);
58169
57099
  return value;
58170
57100
  }
58171
57101
  cacheOpen(ciphertext, plaintext) {
@@ -58175,8 +57105,8 @@ var VaultTransitSecretCipher = class {
58175
57105
  }
58176
57106
  this.openCache.set(ciphertext, plaintext);
58177
57107
  }
58178
- async transit(op, body) {
58179
- const path = `${this.mount}/${op}/${this.key}`;
57108
+ async transit(op, key, body) {
57109
+ const path = `${this.mount}/${op}/${key}`;
58180
57110
  let res = await this.post(path, body, await this.token());
58181
57111
  if (res.status === 403 && this.auth.method === "jwt") {
58182
57112
  this.clientToken = void 0;
@@ -58252,11 +57182,21 @@ async function describeError(res) {
58252
57182
  * as Vault Transit stores ciphertext. The composition root selects one provider,
58253
57183
  * so every store switches together.
58254
57184
  *
57185
+ * Every call carries a {@link SecretScope} naming WHOSE key to use — the
57186
+ * deployment's, or one organization's (docs/designs/per-org-secret-encryption.md).
57187
+ * The scope comes from the caller, never from the stored value.
57188
+ *
58255
57189
  * Contract for an encrypting implementation (e.g. Vault Transit):
58256
- * - `seal` returns a self-describing value (Transit's `vault:v1:…` is already one).
57190
+ * - `seal` returns a self-describing value carrying the envelope version
57191
+ * (`acv1:` + Transit's own `vault:v1:…`).
58257
57192
  * - `open` MUST pass through values it did not seal (no recognizable prefix ⇒
58258
57193
  * return as-is): existing plaintext rows keep working, and re-sealing on the
58259
57194
  * next write migrates them lazily — the flip is online, no backfill required.
57195
+ * Values sealed before scoping existed (a bare `vault:vN:`) open under the
57196
+ * deployment key whatever `scope` says; only the envelope-tagged arm is
57197
+ * scoped.
57198
+ * - A value sealed under one scope MUST NOT open under another. Failing closed
57199
+ * there is the point of passing the scope in.
58260
57200
  * - Neither side ever logs its argument.
58261
57201
  *
58262
57202
  * This is NOT the C5 `SecretsProvider` lease broker (`providers/provider.ts`),
@@ -58297,6 +57237,7 @@ function makeSecretCipher(config) {
58297
57237
  return new VaultTransitSecretCipher({
58298
57238
  addr: config.VAULT_ADDR,
58299
57239
  key: config.VAULT_TRANSIT_KEY,
57240
+ orgKeyPrefix: effectiveOrgKeyPrefix(config.VAULT_TRANSIT_KEY, config.VAULT_TRANSIT_ORG_KEY_PREFIX),
58300
57241
  mount: config.VAULT_TRANSIT_MOUNT,
58301
57242
  ...config.VAULT_NAMESPACE ? { namespace: config.VAULT_NAMESPACE } : {},
58302
57243
  auth