@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.
- package/package.json +1 -1
- package/src/agents/agent-adapter.ts +5 -0
- package/src/agents/gateway-adapter.ts +25 -2
- package/src/cli.ts +187 -113
- package/src/client/collect-agent-picks.ts +5 -2
- package/src/client/daemon-client.ts +29 -18
- package/src/daemon/build-agent-list.ts +8 -6
- package/src/daemon/build-auth-binding.ts +58 -0
- package/src/daemon/build-imp-name.ts +11 -0
- package/src/daemon/build-imp-provider.ts +25 -2
- package/src/daemon/build-payload-hash.ts +4 -3
- package/src/daemon/daemon-connection.ts +277 -20
- package/src/daemon/daemon-context.ts +8 -0
- package/src/daemon/daemon.ts +186 -32
- package/src/daemon/find-token-fingerprint.ts +23 -0
- package/src/daemon/handshake-throttle.ts +47 -0
- package/src/daemon/idempotency-ledger.ts +20 -2
- package/src/daemon/imp-port.ts +4 -2
- package/src/daemon/imp-provider.ts +24 -19
- package/src/daemon/is-allowed-listen-host.ts +63 -0
- package/src/daemon/load-listener-tokens.ts +46 -0
- package/src/daemon/parse-listen-address.ts +33 -0
- package/src/daemon/restore-fleet.ts +2 -1
- package/src/daemon/sessions.ts +6 -0
- package/src/daemon/start-tcp-listener.ts +165 -0
- package/src/federation/build-binding-payload-hash.ts +34 -0
- package/src/federation/build-daemon-outdated-error.ts +14 -0
- package/src/federation/build-events-filter-hash.ts +16 -0
- package/src/federation/build-gateway-error.ts +45 -0
- package/src/federation/build-gateway-id.ts +14 -0
- package/src/federation/build-gateway-result.ts +31 -0
- package/src/federation/build-ruled-value.ts +53 -0
- package/src/federation/collect-unruled-id-paths.ts +46 -0
- package/src/federation/daemon-caller.ts +473 -0
- package/src/federation/daemon-pool.ts +56 -0
- package/src/federation/decode-gateway-cursor.ts +73 -0
- package/src/federation/encode-gateway-cursor.ts +15 -0
- package/src/federation/gateway-error.ts +25 -0
- package/src/federation/gateway-store.ts +253 -0
- package/src/federation/id-rules.ts +85 -0
- package/src/federation/load-gateway-registry.ts +29 -0
- package/src/federation/max-events-cursor-bytes.ts +4 -0
- package/src/federation/max-registry-daemons.ts +26 -0
- package/src/federation/merge-event-pages.ts +228 -0
- package/src/federation/open-gateway-caller.ts +55 -0
- package/src/federation/parse-gateway-id.ts +28 -0
- package/src/federation/parse-gateway-registry.ts +123 -0
- package/src/federation/pick-daemon-state.ts +46 -0
- package/src/federation/plan-event-reads.ts +54 -0
- package/src/federation/read-fleet-events.ts +279 -0
- package/src/federation/require-serving-daemon.ts +27 -0
- package/src/federation/resolve-daemon-request.ts +59 -0
- package/src/federation/routing-caller.ts +450 -0
- package/src/federation/types.ts +33 -0
- package/src/federation/wait-for-outcome.ts +38 -0
- package/src/mcp/answer-rpc-request.ts +14 -1
- package/src/mcp/build-tool-list.ts +6 -5
- package/src/mcp/mcp-tools.ts +41 -8
- package/src/mcp/require-daemon-features.ts +2 -0
- package/src/mcp/run-tool.ts +15 -1
- package/src/mcp/start-mcp-http-server.ts +53 -9
- package/src/mcp/types.ts +8 -1
- package/src/protocol/daemon-features.ts +9 -0
- package/src/protocol/protocol.ts +2 -0
- package/src/protocol/request-param-schemas.ts +6 -0
- package/src/run-daemon-id.ts +52 -0
- package/src/shared/collect-auth-profiles.ts +122 -0
- package/src/shared/collect-gateways.ts +223 -6
- package/src/shared/config.ts +32 -4
- package/src/shared/find-daemon-record.ts +8 -3
- package/src/shared/resolve-auth-profiles.ts +186 -0
- package/src/store/run-migrations.ts +77 -0
- package/src/store/runtime-auth-binding.ts +110 -0
- 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
|
+
}
|
package/src/store/state-store.ts
CHANGED
|
@@ -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
|
+
}
|