@zgeoff/atc 2.19.0 → 2.22.0

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.
Files changed (74) hide show
  1. package/package.json +1 -1
  2. package/src/agents/agent-adapter.ts +5 -0
  3. package/src/agents/gateway-adapter.ts +25 -2
  4. package/src/cli.ts +187 -113
  5. package/src/client/collect-agent-picks.ts +5 -2
  6. package/src/client/daemon-client.ts +29 -18
  7. package/src/daemon/build-agent-list.ts +8 -6
  8. package/src/daemon/build-auth-binding.ts +58 -0
  9. package/src/daemon/build-imp-name.ts +11 -0
  10. package/src/daemon/build-imp-provider.ts +25 -2
  11. package/src/daemon/build-payload-hash.ts +4 -3
  12. package/src/daemon/daemon-connection.ts +277 -20
  13. package/src/daemon/daemon-context.ts +8 -0
  14. package/src/daemon/daemon.ts +186 -32
  15. package/src/daemon/find-token-fingerprint.ts +23 -0
  16. package/src/daemon/handshake-throttle.ts +47 -0
  17. package/src/daemon/idempotency-ledger.ts +20 -2
  18. package/src/daemon/imp-port.ts +4 -2
  19. package/src/daemon/imp-provider.ts +24 -19
  20. package/src/daemon/is-allowed-listen-host.ts +63 -0
  21. package/src/daemon/load-listener-tokens.ts +46 -0
  22. package/src/daemon/parse-listen-address.ts +33 -0
  23. package/src/daemon/restore-fleet.ts +2 -1
  24. package/src/daemon/sessions.ts +6 -0
  25. package/src/daemon/start-tcp-listener.ts +165 -0
  26. package/src/federation/build-binding-payload-hash.ts +34 -0
  27. package/src/federation/build-daemon-outdated-error.ts +14 -0
  28. package/src/federation/build-events-filter-hash.ts +16 -0
  29. package/src/federation/build-gateway-error.ts +45 -0
  30. package/src/federation/build-gateway-id.ts +14 -0
  31. package/src/federation/build-gateway-result.ts +31 -0
  32. package/src/federation/build-ruled-value.ts +53 -0
  33. package/src/federation/collect-unruled-id-paths.ts +46 -0
  34. package/src/federation/daemon-caller.ts +473 -0
  35. package/src/federation/daemon-pool.ts +56 -0
  36. package/src/federation/decode-gateway-cursor.ts +73 -0
  37. package/src/federation/encode-gateway-cursor.ts +15 -0
  38. package/src/federation/gateway-error.ts +25 -0
  39. package/src/federation/gateway-store.ts +253 -0
  40. package/src/federation/id-rules.ts +85 -0
  41. package/src/federation/load-gateway-registry.ts +29 -0
  42. package/src/federation/max-events-cursor-bytes.ts +4 -0
  43. package/src/federation/max-registry-daemons.ts +26 -0
  44. package/src/federation/merge-event-pages.ts +228 -0
  45. package/src/federation/open-gateway-caller.ts +55 -0
  46. package/src/federation/parse-gateway-id.ts +28 -0
  47. package/src/federation/parse-gateway-registry.ts +123 -0
  48. package/src/federation/pick-daemon-state.ts +46 -0
  49. package/src/federation/plan-event-reads.ts +54 -0
  50. package/src/federation/read-fleet-events.ts +279 -0
  51. package/src/federation/require-serving-daemon.ts +27 -0
  52. package/src/federation/resolve-daemon-request.ts +59 -0
  53. package/src/federation/routing-caller.ts +450 -0
  54. package/src/federation/types.ts +33 -0
  55. package/src/federation/wait-for-outcome.ts +38 -0
  56. package/src/mcp/answer-rpc-request.ts +14 -1
  57. package/src/mcp/build-tool-list.ts +6 -5
  58. package/src/mcp/mcp-tools.ts +41 -8
  59. package/src/mcp/require-daemon-features.ts +2 -0
  60. package/src/mcp/run-tool.ts +15 -1
  61. package/src/mcp/start-mcp-http-server.ts +53 -9
  62. package/src/mcp/types.ts +8 -1
  63. package/src/protocol/daemon-features.ts +9 -0
  64. package/src/protocol/protocol.ts +2 -0
  65. package/src/protocol/request-param-schemas.ts +6 -0
  66. package/src/run-daemon-id.ts +52 -0
  67. package/src/shared/collect-auth-profiles.ts +122 -0
  68. package/src/shared/collect-gateways.ts +223 -6
  69. package/src/shared/config.ts +32 -4
  70. package/src/shared/find-daemon-record.ts +8 -3
  71. package/src/shared/resolve-auth-profiles.ts +186 -0
  72. package/src/store/run-migrations.ts +77 -0
  73. package/src/store/runtime-auth-binding.ts +110 -0
  74. package/src/store/state-store.ts +202 -0
@@ -0,0 +1,110 @@
1
+ import type { SessionID } from '../shared/session-id';
2
+
3
+ /**
4
+ * Where a session's runtime auth binding stands:
5
+ *
6
+ * - `provisioning`: an attempt is creating the imp or adding grants.
7
+ * - `ready`: every grant is in place and the session may launch.
8
+ * - `revoked`: atc withdrew the grants; the session never launches again
9
+ * until an explicit rebind.
10
+ * - `revocation_pending`: the binding is blocked, but impd has not yet
11
+ * confirmed every grant gone, so access may remain until the owner
12
+ * resolves it.
13
+ * - `rollback_pending`: a failed attempt left effects atc could not
14
+ * confirm removed.
15
+ * - `rebind_failed`: a rebind failed; the binding keeps its old revision,
16
+ * the failed one is kept beside it, and auth stays blocked until an
17
+ * explicit retry.
18
+ */
19
+ export type RuntimeAuthBindingState =
20
+ | 'provisioning'
21
+ | 'ready'
22
+ | 'revoked'
23
+ | 'revocation_pending'
24
+ | 'rollback_pending'
25
+ | 'rebind_failed';
26
+
27
+ /**
28
+ * A rebind's revision, the binding it moves to, and the attempt making it.
29
+ */
30
+ interface RuntimeAuthRebind {
31
+ readonly revision: number;
32
+ readonly bindingHash: string;
33
+ readonly bindingJSON: string;
34
+ readonly attemptID: string;
35
+ }
36
+
37
+ /**
38
+ * One host's runtime auth binding, keyed by the session that owns the imp;
39
+ * its sub-sessions share it. The binding holds secret names and rules,
40
+ * never a credential value.
41
+ */
42
+ export interface RuntimeAuthBinding {
43
+ readonly hostKey: SessionID;
44
+ readonly target: string;
45
+ readonly targetIdentity: string;
46
+ readonly impName: string;
47
+
48
+ // impd's id for the imp; null until the imp exists.
49
+ readonly impID: string | null;
50
+ readonly revision: number;
51
+ readonly bindingHash: string;
52
+ readonly bindingJSON: string;
53
+ readonly state: RuntimeAuthBindingState;
54
+
55
+ // The attempt that last provisioned this revision.
56
+ readonly attemptID: string;
57
+
58
+ // Whether that attempt created the imp, which only then may be destroyed
59
+ // in its rollback.
60
+ readonly impCreatedByAttempt: boolean;
61
+
62
+ // The next revision a rebind is adding, or the failed one it left; null
63
+ // when no rebind is in flight or failed.
64
+ readonly rebind: RuntimeAuthRebind | null;
65
+ readonly createdAt: number;
66
+ readonly updatedAt: number;
67
+ readonly revokedAt: number | null;
68
+ }
69
+
70
+ // The fields a binding rewrites as it moves between states; `rebind: null`
71
+ // clears a settled rebind.
72
+ export interface RuntimeAuthBindingUpdate {
73
+ readonly state?: RuntimeAuthBindingState;
74
+ readonly impID?: string;
75
+ readonly impCreatedByAttempt?: boolean;
76
+ readonly revision?: number;
77
+ readonly bindingHash?: string;
78
+ readonly bindingJSON?: string;
79
+ readonly attemptID?: string;
80
+ readonly rebind?: RuntimeAuthRebind | null;
81
+ readonly revokedAt?: number;
82
+ }
83
+
84
+ /**
85
+ * Where one secret's grant to a host's imp stands. `uncertain` is a grant a
86
+ * stopped daemon left `granting`, which impd's grant list settles on next
87
+ * use; `revocation_pending` is one atc could not confirm removed.
88
+ */
89
+ export type RuntimeAuthGrantPhase =
90
+ | 'granting'
91
+ | 'granted'
92
+ | 'uncertain'
93
+ | 'revoking'
94
+ | 'revoked'
95
+ | 'revocation_pending';
96
+
97
+ /**
98
+ * One secret's grant to a host's imp: the revision and attempt that added
99
+ * it, and whether impd held it before that attempt, which rules it out of
100
+ * the attempt's rollback.
101
+ */
102
+ export interface RuntimeAuthGrant {
103
+ readonly hostKey: SessionID;
104
+ readonly secret: string;
105
+ readonly revision: number;
106
+ readonly attemptID: string;
107
+ readonly preexisting: boolean;
108
+ readonly phase: RuntimeAuthGrantPhase;
109
+ readonly updatedAt: number;
110
+ }
@@ -24,6 +24,11 @@ import type { MessageOwner } from './message-owner';
24
24
  import type { MessageRecord } from './message-record';
25
25
  import { runMigrations } from './run-migrations';
26
26
  import type { StateStoreSchema } from './run-migrations';
27
+ import type {
28
+ RuntimeAuthBinding,
29
+ RuntimeAuthBindingUpdate,
30
+ RuntimeAuthGrant,
31
+ } from './runtime-auth-binding';
27
32
  import type { TrailEntry } from './trail-entry';
28
33
  import type {
29
34
  MaterializationUpdate,
@@ -780,6 +785,22 @@ export class StateStore {
780
785
  return toIdempotencyRecord(row);
781
786
  }
782
787
 
788
+ // The record a key holds under its principal and operation, or null when
789
+ // the ledger holds none.
790
+ async findIdempotencyKey(
791
+ id: Pick<IdempotencyRecord, 'principal' | 'operation' | 'key'>,
792
+ ): Promise<IdempotencyRecord | null> {
793
+ const row = await this.db
794
+ .selectFrom('idempotency')
795
+ .selectAll()
796
+ .where('principal', '=', id.principal)
797
+ .where('operation', '=', id.operation)
798
+ .where('key', '=', id.key)
799
+ .executeTakeFirst();
800
+
801
+ return row === undefined ? null : toIdempotencyRecord(row);
802
+ }
803
+
783
804
  async updateIdempotencyCompleted(
784
805
  record: Pick<IdempotencyRecord, 'principal' | 'operation' | 'key'>,
785
806
  result: string,
@@ -951,6 +972,154 @@ export class StateStore {
951
972
  .execute();
952
973
  }
953
974
 
975
+ // Records a host's binding as its first attempt starts provisioning it,
976
+ // at the first revision, before the imp exists.
977
+ async createAuthBinding(
978
+ row: Pick<
979
+ RuntimeAuthBinding,
980
+ | 'hostKey'
981
+ | 'target'
982
+ | 'targetIdentity'
983
+ | 'impName'
984
+ | 'bindingHash'
985
+ | 'bindingJSON'
986
+ | 'attemptID'
987
+ >,
988
+ at: number,
989
+ ): Promise<void> {
990
+ await this.db
991
+ .insertInto('runtime_auth_binding')
992
+ .values({
993
+ host_key: row.hostKey,
994
+ target: row.target,
995
+ target_identity: row.targetIdentity,
996
+ imp_name: row.impName,
997
+ imp_id: null,
998
+ revision: 1,
999
+ binding_hash: row.bindingHash,
1000
+ binding_json: row.bindingJSON,
1001
+ state: 'provisioning',
1002
+ attempt_id: row.attemptID,
1003
+ imp_created_by_attempt: 0,
1004
+ rebind_revision: null,
1005
+ rebind_binding_hash: null,
1006
+ rebind_binding_json: null,
1007
+ rebind_attempt_id: null,
1008
+ created_at: at,
1009
+ updated_at: at,
1010
+ revoked_at: null,
1011
+ })
1012
+ .execute();
1013
+ }
1014
+
1015
+ async updateAuthBinding(
1016
+ hostKey: SessionID,
1017
+ fields: RuntimeAuthBindingUpdate,
1018
+ at: number,
1019
+ ): Promise<void> {
1020
+ await this.db
1021
+ .updateTable('runtime_auth_binding')
1022
+ .set({
1023
+ ...(fields.state === undefined ? {} : { state: fields.state }),
1024
+ ...(fields.impID === undefined ? {} : { imp_id: fields.impID }),
1025
+ ...(fields.impCreatedByAttempt === undefined
1026
+ ? {}
1027
+ : { imp_created_by_attempt: fields.impCreatedByAttempt ? 1 : 0 }),
1028
+ ...(fields.revision === undefined ? {} : { revision: fields.revision }),
1029
+ ...(fields.bindingHash === undefined ? {} : { binding_hash: fields.bindingHash }),
1030
+ ...(fields.bindingJSON === undefined ? {} : { binding_json: fields.bindingJSON }),
1031
+ ...(fields.attemptID === undefined ? {} : { attempt_id: fields.attemptID }),
1032
+ ...(fields.rebind === undefined
1033
+ ? {}
1034
+ : {
1035
+ rebind_revision: fields.rebind?.revision ?? null,
1036
+ rebind_binding_hash: fields.rebind?.bindingHash ?? null,
1037
+ rebind_binding_json: fields.rebind?.bindingJSON ?? null,
1038
+ rebind_attempt_id: fields.rebind?.attemptID ?? null,
1039
+ }),
1040
+ ...(fields.revokedAt === undefined ? {} : { revoked_at: fields.revokedAt }),
1041
+ updated_at: at,
1042
+ })
1043
+ .where('host_key', '=', hostKey)
1044
+ .execute();
1045
+ }
1046
+
1047
+ async findAuthBinding(hostKey: SessionID): Promise<RuntimeAuthBinding | null> {
1048
+ const row = await this.db
1049
+ .selectFrom('runtime_auth_binding')
1050
+ .selectAll()
1051
+ .where('host_key', '=', hostKey)
1052
+ .executeTakeFirst();
1053
+
1054
+ return row === undefined ? null : toRuntimeAuthBinding(row);
1055
+ }
1056
+
1057
+ // Writes one secret's grant row for a host, or moves the existing one to
1058
+ // the given phase, revision and attempt.
1059
+ async upsertAuthGrant(grant: Omit<RuntimeAuthGrant, 'updatedAt'>, at: number): Promise<void> {
1060
+ await this.db
1061
+ .insertInto('runtime_auth_grant')
1062
+ .values({
1063
+ host_key: grant.hostKey,
1064
+ secret: grant.secret,
1065
+ revision: grant.revision,
1066
+ attempt_id: grant.attemptID,
1067
+ preexisting: grant.preexisting ? 1 : 0,
1068
+ phase: grant.phase,
1069
+ updated_at: at,
1070
+ })
1071
+ .onConflict((oc) =>
1072
+ oc.columns(['host_key', 'secret']).doUpdateSet((eb) => ({
1073
+ revision: eb.ref('excluded.revision'),
1074
+ attempt_id: eb.ref('excluded.attempt_id'),
1075
+ preexisting: eb.ref('excluded.preexisting'),
1076
+ phase: eb.ref('excluded.phase'),
1077
+ updated_at: eb.ref('excluded.updated_at'),
1078
+ })),
1079
+ )
1080
+ .execute();
1081
+ }
1082
+
1083
+ // A host's grant rows, by secret name.
1084
+ async collectAuthGrants(hostKey: SessionID): Promise<RuntimeAuthGrant[]> {
1085
+ const rows = await this.db
1086
+ .selectFrom('runtime_auth_grant')
1087
+ .selectAll()
1088
+ .where('host_key', '=', hostKey)
1089
+ .orderBy('secret')
1090
+ .execute();
1091
+
1092
+ return rows.map((row) => ({
1093
+ hostKey: toSessionID(row.host_key),
1094
+ secret: row.secret,
1095
+ revision: row.revision,
1096
+ attemptID: row.attempt_id,
1097
+ preexisting: row.preexisting === 1,
1098
+ phase: row.phase,
1099
+ updatedAt: row.updated_at,
1100
+ }));
1101
+ }
1102
+
1103
+ // Drops a host's binding and every grant row it holds together, once
1104
+ // impd confirms the imp and its grants gone.
1105
+ async removeAuthBinding(hostKey: SessionID): Promise<void> {
1106
+ await this.db.transaction().execute(async (trx) => {
1107
+ await trx.deleteFrom('runtime_auth_grant').where('host_key', '=', hostKey).execute();
1108
+ await trx.deleteFrom('runtime_auth_binding').where('host_key', '=', hostKey).execute();
1109
+ });
1110
+ }
1111
+
1112
+ // Runs once as a daemon starts, before it serves a request: a grant still
1113
+ // granting belonged to a daemon that stopped before impd answered, so
1114
+ // whether impd holds it is uncertain until its grant list is read.
1115
+ async reconcileAuthBindings(at: number): Promise<void> {
1116
+ await this.db
1117
+ .updateTable('runtime_auth_grant')
1118
+ .set({ phase: 'uncertain', updated_at: at })
1119
+ .where('phase', '=', 'granting')
1120
+ .execute();
1121
+ }
1122
+
954
1123
  async stop(): Promise<void> {
955
1124
  await this.db.destroy();
956
1125
 
@@ -1334,3 +1503,36 @@ function toWorkspaceMaterialization(
1334
1503
  withheldEnv: parseWithheldEnv(row.withheld_env),
1335
1504
  };
1336
1505
  }
1506
+
1507
+ function toRuntimeAuthBinding(
1508
+ row: Readonly<StateStoreSchema['runtime_auth_binding']>,
1509
+ ): RuntimeAuthBinding {
1510
+ return {
1511
+ hostKey: toSessionID(row.host_key),
1512
+ target: row.target,
1513
+ targetIdentity: row.target_identity,
1514
+ impName: row.imp_name,
1515
+ impID: row.imp_id,
1516
+ revision: row.revision,
1517
+ bindingHash: row.binding_hash,
1518
+ bindingJSON: row.binding_json,
1519
+ state: row.state,
1520
+ attemptID: row.attempt_id,
1521
+ impCreatedByAttempt: row.imp_created_by_attempt === 1,
1522
+ rebind:
1523
+ row.rebind_revision === null ||
1524
+ row.rebind_binding_hash === null ||
1525
+ row.rebind_binding_json === null ||
1526
+ row.rebind_attempt_id === null
1527
+ ? null
1528
+ : {
1529
+ revision: row.rebind_revision,
1530
+ bindingHash: row.rebind_binding_hash,
1531
+ bindingJSON: row.rebind_binding_json,
1532
+ attemptID: row.rebind_attempt_id,
1533
+ },
1534
+ createdAt: row.created_at,
1535
+ updatedAt: row.updated_at,
1536
+ revokedAt: row.revoked_at,
1537
+ };
1538
+ }