@runuai/host 0.9.170 → 0.9.172
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/lib/agents/claude.ts +13 -0
- package/lib/agents/codex.ts +11 -5
- package/lib/agents/session-state.ts +77 -0
- package/lib/agents/transport.ts +2 -0
- package/lib/agents/types.ts +3 -0
- package/lib/agents/uai.ts +3 -25
- package/lib/assignment-evidence.ts +2 -1
- package/lib/capture-flow.ts +14 -1
- package/lib/cloud-credential-leases.ts +9 -3
- package/lib/cloud-credential-runtime.ts +8 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/credential-cleanup-inventory.ts +2 -1
- package/lib/credential-consumers/engines.ts +29 -5
- package/lib/credentials-activation-authority.ts +158 -10
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +299 -0
- package/lib/custom-connection-runtime.ts +70 -0
- package/lib/engine-accounts.ts +1 -1
- package/lib/engine-credential-env.ts +2 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/legacy-runtime-retirement.ts +43 -8
- package/lib/obs.ts +2 -1
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +308 -26
- package/lib/secrets-proxy/address.ts +23 -0
- package/lib/task-credential-cleanup.ts +13 -3
- package/package.json +1 -1
- package/runner/runner.mjs +231 -3
- package/src/capture-wire.ts +102 -0
- package/src/cloud-credentials.ts +36 -372
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +207 -0
- package/src/main.ts +116 -3
- package/src/protocol.ts +111 -0
package/src/cloud-credentials.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { LEASE_PARAMETER_ROOT, BROKER_PARAMETER_ROOT, MAX_CREDENTIAL_ID_CHARS, MAX_PARAMETER_NAME_CHARS, leaseParameterName, brokerParameterName, idPathSegment, parseIdPathSegment, isEngineKind, parseLeaseParameterName, parseBrokerParameterName, sameLocation, isIdValue, isTagValue, type CredentialClass, type CredentialScope, type CredentialLocation } from "./credential-location";
|
|
2
|
+
export * from "./credential-location";
|
|
3
|
+
import { parseCaptureFlowWire, parseCaptureResultWire, type CaptureFlowWire, type CaptureResultWire } from "./capture-wire";
|
|
4
|
+
export * from "./capture-wire";
|
|
5
|
+
import { exactObject, isPositiveInteger, isEpochMs } from "./credential-wire-validation";
|
|
6
|
+
import { parseCustomConnectionMaterial, parseCustomConnectionLease, parseCustomConnectionEnabled, type CustomConnectionMaterial, type CustomConnectionLease, type CustomConnectionCombination } from "./custom-connections";
|
|
7
|
+
export * from "./custom-connections";
|
|
1
8
|
import { MAX_TASK_SECRETS, TASK_SECRET_NAME, TASK_SECRET_STAGING_ID, type TaskSecretManifestEntry } from "./task-secrets";
|
|
2
9
|
|
|
3
10
|
/**
|
|
@@ -55,233 +62,6 @@ export const TASK_MACHINE_CREDENTIALS_V1_ENV = "UAI_TASK_MACHINE_CREDENTIALS_V1"
|
|
|
55
62
|
// Parameter layout
|
|
56
63
|
// ---------------------------------------------------------------------------
|
|
57
64
|
|
|
58
|
-
/** What a task may receive: a static key, or an OAuth login's brokered copy. */
|
|
59
|
-
export const LEASE_PARAMETER_ROOT = "/uai/lease";
|
|
60
|
-
/**
|
|
61
|
-
* The full OAuth material only the broker reads. Outside `/uai` altogether:
|
|
62
|
-
* `GetParametersByPath` returns every descendant of a path its caller may
|
|
63
|
-
* read, even one a Deny names, so containment comes from the root alone.
|
|
64
|
-
*/
|
|
65
|
-
export const BROKER_PARAMETER_ROOT = "/uai-broker";
|
|
66
|
-
|
|
67
|
-
export type CredentialClass = "engine" | "mcp" | "github" | "ssh";
|
|
68
|
-
export type CredentialScope = "org" | "user";
|
|
69
|
-
|
|
70
|
-
/** Where one revision of one credential lives. */
|
|
71
|
-
export interface CredentialLocation {
|
|
72
|
-
class: CredentialClass;
|
|
73
|
-
/** `github` and `ssh` are always `user`: one per person. */
|
|
74
|
-
scope: CredentialScope;
|
|
75
|
-
/** The org id for `org` scope, the user id for `user` scope. */
|
|
76
|
-
ownerId: string;
|
|
77
|
-
/** `engine` only: the engine kind (`codex`, `claude`, …). */
|
|
78
|
-
engineKind?: string;
|
|
79
|
-
/** `engine` and `mcp` only: the credential's stable id. */
|
|
80
|
-
credentialId?: string;
|
|
81
|
-
/** One immutable revision of the material. */
|
|
82
|
-
revision: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** SSM allows `[A-Za-z0-9_.-]` in a path segment. An id that already fits,
|
|
86
|
-
* starts with a letter or digit and has no dot is used as it is; any other
|
|
87
|
-
* id is `-` followed by the hex of its UTF-8 bytes. No plain id starts with
|
|
88
|
-
* `-`, so the mapping is injective. Legacy workspace ids
|
|
89
|
-
* (`personal:<user>`) take the second form. */
|
|
90
|
-
const PLAIN_ID_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
91
|
-
const ENCODED_ID_SEGMENT = /^-(?:[0-9a-f]{2})+$/;
|
|
92
|
-
const ENGINE_KIND = /^[a-z][a-z0-9-]{0,31}$/;
|
|
93
|
-
export const MAX_CREDENTIAL_ID_CHARS = 128;
|
|
94
|
-
/** SSM's limit for a parameter name, its ARN included. */
|
|
95
|
-
export const MAX_PARAMETER_NAME_CHARS = 1011;
|
|
96
|
-
|
|
97
|
-
function isIdValue(value: unknown): value is string {
|
|
98
|
-
return (
|
|
99
|
-
typeof value === "string" &&
|
|
100
|
-
value.length > 0 &&
|
|
101
|
-
value.length <= MAX_CREDENTIAL_ID_CHARS &&
|
|
102
|
-
!/[\u0000-\u001f\u007f]/.test(value)
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function utf8Hex(value: string): string {
|
|
107
|
-
let out = "";
|
|
108
|
-
for (const byte of new TextEncoder().encode(value)) {
|
|
109
|
-
out += byte.toString(16).padStart(2, "0");
|
|
110
|
-
}
|
|
111
|
-
return out;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function fromUtf8Hex(hex: string): string | null {
|
|
115
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
116
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
117
|
-
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
121
|
-
} catch {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** The path segment for an id (see `PLAIN_ID_SEGMENT`). Throws on an id no
|
|
127
|
-
* segment may carry: empty, too long, or containing control characters. */
|
|
128
|
-
export function idPathSegment(id: string): string {
|
|
129
|
-
if (!isIdValue(id)) throw new Error("invalid credential path id");
|
|
130
|
-
return PLAIN_ID_SEGMENT.test(id) ? id : `-${utf8Hex(id)}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Inverse of `idPathSegment`; null for a segment it never produces. */
|
|
134
|
-
export function parseIdPathSegment(segment: string): string | null {
|
|
135
|
-
if (PLAIN_ID_SEGMENT.test(segment)) {
|
|
136
|
-
return segment.length <= MAX_CREDENTIAL_ID_CHARS ? segment : null;
|
|
137
|
-
}
|
|
138
|
-
if (!ENCODED_ID_SEGMENT.test(segment)) return null;
|
|
139
|
-
const id = fromUtf8Hex(segment.slice(1));
|
|
140
|
-
if (id === null || !isIdValue(id) || PLAIN_ID_SEGMENT.test(id)) return null;
|
|
141
|
-
return id;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function isEngineKind(value: unknown): value is string {
|
|
145
|
-
return typeof value === "string" && ENGINE_KIND.test(value);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function locationSegments(location: CredentialLocation): string[] {
|
|
149
|
-
const owner = location.scope === "org" ? "orgs" : "users";
|
|
150
|
-
const segments = [owner, idPathSegment(location.ownerId)];
|
|
151
|
-
switch (location.class) {
|
|
152
|
-
case "engine": {
|
|
153
|
-
if (!isEngineKind(location.engineKind)) {
|
|
154
|
-
throw new Error("invalid engine kind");
|
|
155
|
-
}
|
|
156
|
-
if (location.credentialId === undefined) {
|
|
157
|
-
throw new Error("an engine credential needs its id");
|
|
158
|
-
}
|
|
159
|
-
segments.push(
|
|
160
|
-
"engines",
|
|
161
|
-
location.engineKind,
|
|
162
|
-
idPathSegment(location.credentialId),
|
|
163
|
-
);
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
case "mcp": {
|
|
167
|
-
if (location.engineKind !== undefined) {
|
|
168
|
-
throw new Error("an MCP credential has no engine kind");
|
|
169
|
-
}
|
|
170
|
-
if (location.credentialId === undefined) {
|
|
171
|
-
throw new Error("an MCP credential needs its id");
|
|
172
|
-
}
|
|
173
|
-
segments.push("mcp", idPathSegment(location.credentialId));
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
case "github":
|
|
177
|
-
case "ssh": {
|
|
178
|
-
if (location.scope !== "user") {
|
|
179
|
-
throw new Error(`${location.class} credentials belong to a user`);
|
|
180
|
-
}
|
|
181
|
-
if (
|
|
182
|
-
location.engineKind !== undefined ||
|
|
183
|
-
location.credentialId !== undefined
|
|
184
|
-
) {
|
|
185
|
-
throw new Error(`${location.class} has one credential per person`);
|
|
186
|
-
}
|
|
187
|
-
segments.push(location.class);
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
default:
|
|
191
|
-
throw new Error("invalid credential class");
|
|
192
|
-
}
|
|
193
|
-
segments.push(idPathSegment(location.revision));
|
|
194
|
-
return segments;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function nameUnder(root: string, location: CredentialLocation): string {
|
|
198
|
-
const name = `${root}/${locationSegments(location).join("/")}`;
|
|
199
|
-
if (name.length > MAX_PARAMETER_NAME_CHARS) {
|
|
200
|
-
throw new Error("credential parameter name too long");
|
|
201
|
-
}
|
|
202
|
-
return name;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** The lease-readable parameter of one revision. */
|
|
206
|
-
export function leaseParameterName(location: CredentialLocation): string {
|
|
207
|
-
return nameUnder(LEASE_PARAMETER_ROOT, location);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** The broker-only parameter of one revision of an OAuth credential. GitHub
|
|
211
|
-
* tokens and SSH keys are static and have none. */
|
|
212
|
-
export function brokerParameterName(location: CredentialLocation): string {
|
|
213
|
-
if (location.class === "github" || location.class === "ssh") {
|
|
214
|
-
throw new Error(`${location.class} credentials have no broker part`);
|
|
215
|
-
}
|
|
216
|
-
return nameUnder(BROKER_PARAMETER_ROOT, location);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function parseUnder(root: string, name: string): CredentialLocation | null {
|
|
220
|
-
if (
|
|
221
|
-
typeof name !== "string" ||
|
|
222
|
-
name.length > MAX_PARAMETER_NAME_CHARS ||
|
|
223
|
-
!name.startsWith(`${root}/`)
|
|
224
|
-
) {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
const parts = name.slice(root.length + 1).split("/");
|
|
228
|
-
const [ownerKind, ownerSegment] = parts;
|
|
229
|
-
const scope: CredentialScope | null =
|
|
230
|
-
ownerKind === "orgs" ? "org" : ownerKind === "users" ? "user" : null;
|
|
231
|
-
if (scope === null || ownerSegment === undefined) return null;
|
|
232
|
-
const ownerId = parseIdPathSegment(ownerSegment);
|
|
233
|
-
if (ownerId === null) return null;
|
|
234
|
-
const revisionOf = (segment: string | undefined): string | null =>
|
|
235
|
-
segment === undefined ? null : parseIdPathSegment(segment);
|
|
236
|
-
|
|
237
|
-
if (parts[2] === "engines" && parts.length === 6) {
|
|
238
|
-
const credentialId = parseIdPathSegment(parts[4]!);
|
|
239
|
-
const revision = revisionOf(parts[5]);
|
|
240
|
-
if (!isEngineKind(parts[3]) || credentialId === null || revision === null) {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
return {
|
|
244
|
-
class: "engine",
|
|
245
|
-
scope,
|
|
246
|
-
ownerId,
|
|
247
|
-
engineKind: parts[3],
|
|
248
|
-
credentialId,
|
|
249
|
-
revision,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
if (parts[2] === "mcp" && parts.length === 5) {
|
|
253
|
-
const credentialId = parseIdPathSegment(parts[3]!);
|
|
254
|
-
const revision = revisionOf(parts[4]);
|
|
255
|
-
if (credentialId === null || revision === null) return null;
|
|
256
|
-
return { class: "mcp", scope, ownerId, credentialId, revision };
|
|
257
|
-
}
|
|
258
|
-
if (
|
|
259
|
-
(parts[2] === "github" || parts[2] === "ssh") &&
|
|
260
|
-
parts.length === 4 &&
|
|
261
|
-
scope === "user"
|
|
262
|
-
) {
|
|
263
|
-
const revision = revisionOf(parts[3]);
|
|
264
|
-
if (revision === null) return null;
|
|
265
|
-
return { class: parts[2], scope, ownerId, revision };
|
|
266
|
-
}
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export function parseLeaseParameterName(
|
|
271
|
-
name: string,
|
|
272
|
-
): CredentialLocation | null {
|
|
273
|
-
return parseUnder(LEASE_PARAMETER_ROOT, name);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function parseBrokerParameterName(
|
|
277
|
-
name: string,
|
|
278
|
-
): CredentialLocation | null {
|
|
279
|
-
const location = parseUnder(BROKER_PARAMETER_ROOT, name);
|
|
280
|
-
return location && (location.class === "engine" || location.class === "mcp")
|
|
281
|
-
? location
|
|
282
|
-
: null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
65
|
// ---------------------------------------------------------------------------
|
|
286
66
|
// Lease-readable values
|
|
287
67
|
// ---------------------------------------------------------------------------
|
|
@@ -340,6 +120,7 @@ interface LeasedValueBase {
|
|
|
340
120
|
}
|
|
341
121
|
|
|
342
122
|
export type LeasedValue =
|
|
123
|
+
| (LeasedValueBase & { class: "engine"; authKind: "custom-connection"; connection: CustomConnectionMaterial })
|
|
343
124
|
| (LeasedValueBase & { class: "engine"; authKind: "env"; env: Record<string, string> })
|
|
344
125
|
| (LeasedValueBase & {
|
|
345
126
|
class: "engine";
|
|
@@ -374,16 +155,6 @@ const FORBIDDEN_HEADER_NAMES = new Set([
|
|
|
374
155
|
"upgrade",
|
|
375
156
|
]);
|
|
376
157
|
|
|
377
|
-
function sameLocation(a: CredentialLocation, b: CredentialLocation): boolean {
|
|
378
|
-
return (
|
|
379
|
-
a.class === b.class &&
|
|
380
|
-
a.scope === b.scope &&
|
|
381
|
-
a.ownerId === b.ownerId &&
|
|
382
|
-
a.engineKind === b.engineKind &&
|
|
383
|
-
a.credentialId === b.credentialId &&
|
|
384
|
-
a.revision === b.revision
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
158
|
|
|
388
159
|
function parseLocationField(value: unknown): CredentialLocation | null {
|
|
389
160
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
@@ -475,6 +246,10 @@ export function parseLeasedValue(
|
|
|
475
246
|
switch (location.class) {
|
|
476
247
|
case "engine": {
|
|
477
248
|
const kind = location.engineKind!;
|
|
249
|
+
if (o.authKind === "custom-connection") {
|
|
250
|
+
const connection = exactObject(o, ["v", "part", "location", "class", "authKind", "connection"]) ? parseCustomConnectionMaterial(o.connection) : null;
|
|
251
|
+
return connection && connection.engine === kind ? { ...base, class: "engine", authKind: "custom-connection", connection } : null;
|
|
252
|
+
}
|
|
478
253
|
if (o.authKind === "env") {
|
|
479
254
|
const env = exactObject(o, ["v", "part", "location", "class", "authKind", "env"])
|
|
480
255
|
? parseStringMap(o.env, ENGINE_ENV_CREDENTIAL_KEYS[kind] ?? [], 4096)
|
|
@@ -559,104 +334,6 @@ export function parseLeasedValue(
|
|
|
559
334
|
/** Advertised by a sign-in box that writes captures as ADR-154 revisions. */
|
|
560
335
|
export const SIGNIN_BOX_CAPTURE_FLOWS_FEATURE = "signin-box-capture-flows-v1";
|
|
561
336
|
|
|
562
|
-
/**
|
|
563
|
-
* What the cloud tells the sign-in box to write for one capture: the
|
|
564
|
-
* revision's parameters (the box never chooses a name) and the ids it tags
|
|
565
|
-
* them with. The box checks every field against the others before it writes.
|
|
566
|
-
*/
|
|
567
|
-
export interface CaptureFlowWire {
|
|
568
|
-
credentialId: string;
|
|
569
|
-
revision: string;
|
|
570
|
-
leaseParameter: string;
|
|
571
|
-
/** Null for a static credential. */
|
|
572
|
-
brokerParameter: string | null;
|
|
573
|
-
/** The workspace tag: required except for GitHub and SSH. */
|
|
574
|
-
orgId: string | null;
|
|
575
|
-
/** The person, for a personal credential. */
|
|
576
|
-
userId: string | null;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/** What the box answers once both parts are written: never material. */
|
|
580
|
-
export interface CaptureResultWire {
|
|
581
|
-
credentialId: string;
|
|
582
|
-
revision: string;
|
|
583
|
-
/** The lease-readable parameter's Parameter Store version. */
|
|
584
|
-
leaseVersion: number;
|
|
585
|
-
/** The access token's expiry when the box could read it. */
|
|
586
|
-
expiresAt: number | null;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* A capture flow, rebuilt and checked, with the location its parameters name,
|
|
591
|
-
* or null. The two parameters must name the same credential and revision under
|
|
592
|
-
* their own roots, and the tags must be the parameter's owner: a workspace
|
|
593
|
-
* credential's `orgId` is its owner and it has no `userId`; a personal AI
|
|
594
|
-
* account or MCP connection names both its person and its workspace; GitHub
|
|
595
|
-
* and SSH name their person alone.
|
|
596
|
-
*/
|
|
597
|
-
export function parseCaptureFlowWire(
|
|
598
|
-
value: unknown,
|
|
599
|
-
): (CaptureFlowWire & { location: CredentialLocation }) | null {
|
|
600
|
-
const o = exactObject(value, [
|
|
601
|
-
"credentialId",
|
|
602
|
-
"revision",
|
|
603
|
-
"leaseParameter",
|
|
604
|
-
"brokerParameter",
|
|
605
|
-
"orgId",
|
|
606
|
-
"userId",
|
|
607
|
-
]);
|
|
608
|
-
if (
|
|
609
|
-
!o ||
|
|
610
|
-
!isIdValue(o.credentialId) ||
|
|
611
|
-
!isIdValue(o.revision) ||
|
|
612
|
-
typeof o.leaseParameter !== "string" ||
|
|
613
|
-
!(o.brokerParameter === null || typeof o.brokerParameter === "string") ||
|
|
614
|
-
!(o.orgId === null || (isIdValue(o.orgId) && isTagValue(o.orgId))) ||
|
|
615
|
-
!(o.userId === null || (isIdValue(o.userId) && isTagValue(o.userId)))
|
|
616
|
-
) {
|
|
617
|
-
return null;
|
|
618
|
-
}
|
|
619
|
-
const location = parseLeaseParameterName(o.leaseParameter);
|
|
620
|
-
if (!location || location.revision !== o.revision) return null;
|
|
621
|
-
const isPerson = location.class === "github" || location.class === "ssh";
|
|
622
|
-
if (!isPerson && location.credentialId !== o.credentialId) return null;
|
|
623
|
-
if (o.brokerParameter !== null) {
|
|
624
|
-
const broker = parseBrokerParameterName(o.brokerParameter);
|
|
625
|
-
if (!broker || !sameLocation(broker, location)) return null;
|
|
626
|
-
}
|
|
627
|
-
if (isPerson && o.brokerParameter !== null) return null;
|
|
628
|
-
const ownerOk =
|
|
629
|
-
location.scope === "org"
|
|
630
|
-
? o.orgId === location.ownerId && o.userId === null
|
|
631
|
-
: o.userId === location.ownerId && (isPerson ? o.orgId === null : o.orgId !== null);
|
|
632
|
-
if (!ownerOk) return null;
|
|
633
|
-
return {
|
|
634
|
-
credentialId: o.credentialId,
|
|
635
|
-
revision: o.revision,
|
|
636
|
-
leaseParameter: o.leaseParameter,
|
|
637
|
-
brokerParameter: o.brokerParameter,
|
|
638
|
-
orgId: o.orgId,
|
|
639
|
-
userId: o.userId,
|
|
640
|
-
location,
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
export function parseCaptureResultWire(value: unknown): CaptureResultWire | null {
|
|
645
|
-
const o = exactObject(value, ["credentialId", "revision", "leaseVersion", "expiresAt"]);
|
|
646
|
-
return o &&
|
|
647
|
-
isIdValue(o.credentialId) &&
|
|
648
|
-
isIdValue(o.revision) &&
|
|
649
|
-
isPositiveInteger(o.leaseVersion) &&
|
|
650
|
-
(o.expiresAt === null || isEpochMs(o.expiresAt))
|
|
651
|
-
? {
|
|
652
|
-
credentialId: o.credentialId,
|
|
653
|
-
revision: o.revision,
|
|
654
|
-
leaseVersion: o.leaseVersion,
|
|
655
|
-
expiresAt: o.expiresAt,
|
|
656
|
-
}
|
|
657
|
-
: null;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
337
|
// ---------------------------------------------------------------------------
|
|
661
338
|
// Session tags
|
|
662
339
|
// ---------------------------------------------------------------------------
|
|
@@ -672,15 +349,6 @@ export const LEASE_V1_SESSION_TAG = { key: "lease", value: "v1" } as const;
|
|
|
672
349
|
* which resource-tag conditions compare. */
|
|
673
350
|
export const ORG_PATH_SESSION_TAG_KEY = "orgseg";
|
|
674
351
|
|
|
675
|
-
const TAG_VALUE = /^[\p{L}\p{Z}\p{N}_.:/=+\-@]{1,256}$/u;
|
|
676
|
-
|
|
677
|
-
/** STS session tag and SSM resource tag values: the same grammar, refused
|
|
678
|
-
* rather than rewritten, so a principal tag and a resource tag written for
|
|
679
|
-
* one id always compare equal. */
|
|
680
|
-
export function isTagValue(value: unknown): value is string {
|
|
681
|
-
return typeof value === "string" && TAG_VALUE.test(value);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
352
|
// ---------------------------------------------------------------------------
|
|
685
353
|
// The v1 grant and its manifest
|
|
686
354
|
// ---------------------------------------------------------------------------
|
|
@@ -734,7 +402,8 @@ export interface LeaseManifestEngine {
|
|
|
734
402
|
engineKind: string;
|
|
735
403
|
scope: CredentialScope;
|
|
736
404
|
label: string;
|
|
737
|
-
authKind: "env" | "config-dir";
|
|
405
|
+
authKind: "env" | "config-dir" | "custom-connection";
|
|
406
|
+
connection?: CustomConnectionLease;
|
|
738
407
|
/** Position within its scope; the list itself is already in try order. */
|
|
739
408
|
position: number;
|
|
740
409
|
}
|
|
@@ -763,6 +432,10 @@ export interface LeaseManifestStatic {
|
|
|
763
432
|
|
|
764
433
|
/** What a task may read, by name and version — never a value. */
|
|
765
434
|
export interface CredentialManifest {
|
|
435
|
+
/** Platform connections.enabled, included in the digest; absent fails closed. */
|
|
436
|
+
connectionsEnabled?: CustomConnectionCombination[];
|
|
437
|
+
/** Pins refused by connection policy, never secret material. Omitted if empty. */
|
|
438
|
+
connectionsUnsupported?: string[];
|
|
766
439
|
/** Omitted on older peers and when empty, preserving old digests. */
|
|
767
440
|
taskSecrets?: TaskSecretManifestEntry[];
|
|
768
441
|
/** The task's projects, lead first. */
|
|
@@ -956,22 +629,6 @@ export interface CredentialsActivatedFrame {
|
|
|
956
629
|
|
|
957
630
|
type WireObject = Record<string, unknown>;
|
|
958
631
|
|
|
959
|
-
function exactObject(
|
|
960
|
-
value: unknown,
|
|
961
|
-
required: readonly string[],
|
|
962
|
-
optional: readonly string[] = [],
|
|
963
|
-
): WireObject | null {
|
|
964
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
965
|
-
const object = value as WireObject;
|
|
966
|
-
const keys = Object.keys(object);
|
|
967
|
-
if (
|
|
968
|
-
keys.some(key => !required.includes(key) && !optional.includes(key)) ||
|
|
969
|
-
required.some((key) => !Object.hasOwn(object, key))
|
|
970
|
-
) {
|
|
971
|
-
return null;
|
|
972
|
-
}
|
|
973
|
-
return object;
|
|
974
|
-
}
|
|
975
632
|
|
|
976
633
|
function isBoundedString(value: unknown, max: number): value is string {
|
|
977
634
|
return typeof value === "string" && value.length > 0 && value.length <= max;
|
|
@@ -981,17 +638,13 @@ function isOpId(value: unknown): value is string {
|
|
|
981
638
|
return isBoundedString(value, MAX_LEASE_OP_ID_CHARS) && OP_ID.test(value);
|
|
982
639
|
}
|
|
983
640
|
|
|
984
|
-
|
|
985
|
-
return Number.isSafeInteger(value) && (value as number) > 0;
|
|
986
|
-
}
|
|
641
|
+
|
|
987
642
|
|
|
988
643
|
function isNonNegativeInteger(value: unknown): value is number {
|
|
989
644
|
return Number.isSafeInteger(value) && (value as number) >= 0;
|
|
990
645
|
}
|
|
991
646
|
|
|
992
|
-
|
|
993
|
-
return isPositiveInteger(value);
|
|
994
|
-
}
|
|
647
|
+
|
|
995
648
|
|
|
996
649
|
function isScope(value: unknown): value is CredentialScope {
|
|
997
650
|
return value === "org" || value === "user";
|
|
@@ -1058,7 +711,7 @@ function parseEngine(
|
|
|
1058
711
|
"label",
|
|
1059
712
|
"authKind",
|
|
1060
713
|
"position",
|
|
1061
|
-
]);
|
|
714
|
+
], ["connection"]);
|
|
1062
715
|
if (
|
|
1063
716
|
!o ||
|
|
1064
717
|
!isIdValue(o.credentialId) ||
|
|
@@ -1071,11 +724,13 @@ function parseEngine(
|
|
|
1071
724
|
!isEngineKind(o.engineKind) ||
|
|
1072
725
|
!isScope(o.scope) ||
|
|
1073
726
|
!isBoundedString(o.label, MAX_MANIFEST_LABEL_CHARS) ||
|
|
1074
|
-
(o.authKind !== "env" && o.authKind !== "config-dir") ||
|
|
727
|
+
(o.authKind !== "env" && o.authKind !== "config-dir" && o.authKind !== "custom-connection") ||
|
|
1075
728
|
!isNonNegativeInteger(o.position)
|
|
1076
729
|
) {
|
|
1077
730
|
return null;
|
|
1078
731
|
}
|
|
732
|
+
const connection = o.connection === undefined ? undefined : parseCustomConnectionLease(o.connection, o.engineKind);
|
|
733
|
+
if (connection === null || ((o.authKind === "custom-connection") !== (connection !== undefined)) || (connection && (connection.expiresAt ?? null) !== o.expiresAt)) return null;
|
|
1079
734
|
const location = parseLeaseParameterName(o.parameter);
|
|
1080
735
|
if (
|
|
1081
736
|
!location ||
|
|
@@ -1100,6 +755,7 @@ function parseEngine(
|
|
|
1100
755
|
label: o.label,
|
|
1101
756
|
authKind: o.authKind,
|
|
1102
757
|
position: o.position,
|
|
758
|
+
...(connection ? { connection } : {}),
|
|
1103
759
|
};
|
|
1104
760
|
}
|
|
1105
761
|
|
|
@@ -1243,7 +899,7 @@ function parseManifest(
|
|
|
1243
899
|
grant: { orgId: string; userId: string; taskId: string },
|
|
1244
900
|
credentialSessions: ReadonlySet<string>,
|
|
1245
901
|
): CredentialManifest | null {
|
|
1246
|
-
const o = exactObject(value, ["projects", "engines", "mcp", "github", "ssh"], ["taskSecrets"]);
|
|
902
|
+
const o = exactObject(value, ["projects", "engines", "mcp", "github", "ssh"], ["taskSecrets", "connectionsEnabled", "connectionsUnsupported"]);
|
|
1247
903
|
if (
|
|
1248
904
|
!o ||
|
|
1249
905
|
!Array.isArray(o.projects) ||
|
|
@@ -1255,6 +911,14 @@ function parseManifest(
|
|
|
1255
911
|
) {
|
|
1256
912
|
return null;
|
|
1257
913
|
}
|
|
914
|
+
const connectionsEnabled = o.connectionsEnabled === undefined ? undefined : parseCustomConnectionEnabled(o.connectionsEnabled);
|
|
915
|
+
if (connectionsEnabled === null) return null;
|
|
916
|
+
const connectionsUnsupported = o.connectionsUnsupported;
|
|
917
|
+
if (connectionsUnsupported !== undefined && (!Array.isArray(connectionsUnsupported) ||
|
|
918
|
+
connectionsUnsupported.length === 0 || connectionsUnsupported.length > MAX_MANIFEST_ENGINES ||
|
|
919
|
+
!connectionsUnsupported.every(isIdValue) || new Set(connectionsUnsupported).size !== connectionsUnsupported.length)) return null;
|
|
920
|
+
const customManifest = { ...(connectionsEnabled ? { connectionsEnabled } : {}),
|
|
921
|
+
...(connectionsUnsupported ? { connectionsUnsupported: [...connectionsUnsupported] as string[] } : {}) };
|
|
1258
922
|
const projects: LeaseManifestProject[] = [];
|
|
1259
923
|
for (const item of o.projects) {
|
|
1260
924
|
const p = exactObject(item, ["projectId", "sessionId"]);
|
|
@@ -1316,9 +980,9 @@ function parseManifest(
|
|
|
1316
980
|
entries.push(e as unknown as TaskSecretManifestEntry);
|
|
1317
981
|
}
|
|
1318
982
|
if (!uniqueBy(entries, e => e.name)) return null;
|
|
1319
|
-
return { projects, engines, mcp, github, ssh, taskSecrets: entries };
|
|
983
|
+
return { projects, engines, mcp, github, ssh, taskSecrets: entries, ...customManifest };
|
|
1320
984
|
}
|
|
1321
|
-
return { projects, engines, mcp, github, ssh };
|
|
985
|
+
return { projects, engines, mcp, github, ssh, ...customManifest };
|
|
1322
986
|
}
|
|
1323
987
|
|
|
1324
988
|
function parseFiles(value: unknown): LeaseFiles | null {
|