@nylorun/runtime 0.28.0-beta → 0.30.0-beta
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/CHANGELOG.md +47 -1
- package/README.md +1 -1
- package/dist/api/components.d.ts +109 -2
- package/dist/api/http/define.d.ts +4 -0
- package/dist/api/http/define.js +20 -9
- package/dist/api/http/routes/agent-manifests.d.ts +1 -1
- package/dist/api/http/routes/agent-manifests.js +1 -1
- package/dist/api/http/routes/agents.js +6 -6
- package/dist/api/http/routes/reads.js +29 -4
- package/dist/api/http/routes/tenant.js +5 -2
- package/dist/api/http/routes/triggers.js +15 -12
- package/dist/api/openapi.js +3 -3
- package/dist/gates/egress-credentials.js +2 -13
- package/dist/gates/handler.js +2 -1
- package/dist/gates/http-tool.js +1 -1
- package/dist/gates/in-process.d.ts +9 -0
- package/dist/gates/in-process.js +21 -0
- package/dist/gates/meter.js +16 -0
- package/dist/gates/tenant-vaults.d.ts +1 -1
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/main.js +3 -0
- package/dist/host/stack-config.d.ts +10 -0
- package/dist/host/stack-config.js +36 -0
- package/dist/management-openapi.json +492 -80
- package/dist/mcp/connect.d.ts +36 -7
- package/dist/mcp/connect.js +76 -15
- package/dist/mcp/gateway-outcomes.d.ts +14 -0
- package/dist/mcp/gateway-outcomes.js +46 -0
- package/dist/mcp/pool.js +5 -0
- package/dist/model/call-headers.d.ts +15 -0
- package/dist/model/call-headers.js +9 -0
- package/dist/model/models.d.ts +7 -0
- package/dist/model/models.js +3 -0
- package/dist/model/pi-model.d.ts +6 -0
- package/dist/model/pi-model.js +36 -3
- package/dist/openapi.json +331 -215
- package/dist/store/postgres/drizzle/0037_model_usage_cost_split.sql +4 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
- package/dist/store/postgres/reads.js +31 -1
- package/dist/store/postgres/schema.d.ts +78 -5
- package/dist/store/postgres/schema.js +12 -3
- package/dist/tenant/agents.js +7 -3
- package/dist/tenant/transcript.d.ts +8 -0
- package/dist/tenant/transcript.js +2 -0
- package/dist/tenant/types.d.ts +5 -0
- package/dist/vault/headers.d.ts +8 -0
- package/dist/vault/headers.js +25 -0
- package/dist/vault/host-model.js +8 -5
- package/dist/vault/service.d.ts +12 -5
- package/dist/vault/service.js +43 -6
- package/dist/vault/sources.js +22 -1
- package/dist/vault/workload-token.d.ts +16 -0
- package/dist/vault/workload-token.js +58 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
ALTER TABLE "nylorun"."model_usage" ADD COLUMN "cost_input_usd" double precision;--> statement-breakpoint
|
|
2
|
+
ALTER TABLE "nylorun"."model_usage" ADD COLUMN "cost_cache_read_usd" double precision;--> statement-breakpoint
|
|
3
|
+
ALTER TABLE "nylorun"."model_usage" ADD COLUMN "cost_cache_write_usd" double precision;--> statement-breakpoint
|
|
4
|
+
ALTER TABLE "nylorun"."model_usage" ADD COLUMN "cost_output_usd" double precision;
|
|
@@ -32,6 +32,18 @@ function grantsWhere(column, grants) {
|
|
|
32
32
|
function accessWhere(access) {
|
|
33
33
|
return and(access.owner === undefined ? undefined : eq(sessions.ownerUserId, access.owner), access.agents === undefined ? undefined : grantsWhere(sessions.agentId, access.agents));
|
|
34
34
|
}
|
|
35
|
+
/** A row's cost by token kind, when the adapter kept it (migration 0037). */
|
|
36
|
+
function costSplitOf(row) {
|
|
37
|
+
const { costInputUsd, costCacheReadUsd, costCacheWriteUsd, costOutputUsd } = row;
|
|
38
|
+
if (costInputUsd === null || costCacheReadUsd === null || costCacheWriteUsd === null || costOutputUsd === null)
|
|
39
|
+
return null;
|
|
40
|
+
return {
|
|
41
|
+
input: costInputUsd,
|
|
42
|
+
cacheRead: costCacheReadUsd,
|
|
43
|
+
cacheWrite: costCacheWriteUsd,
|
|
44
|
+
output: costOutputUsd,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
35
47
|
function publicCall(row) {
|
|
36
48
|
return {
|
|
37
49
|
id: row.id,
|
|
@@ -42,6 +54,8 @@ function publicCall(row) {
|
|
|
42
54
|
provider: row.provider,
|
|
43
55
|
model: row.model,
|
|
44
56
|
at: row.createdAt,
|
|
57
|
+
startedAt: row.startedAt,
|
|
58
|
+
endedAt: row.endedAt,
|
|
45
59
|
outcome: "completed",
|
|
46
60
|
usage: {
|
|
47
61
|
inputTokens: row.inputTokens,
|
|
@@ -54,6 +68,7 @@ function publicCall(row) {
|
|
|
54
68
|
duplicate: row.duplicate,
|
|
55
69
|
tokensReported: row.tokensReported,
|
|
56
70
|
costKnown: row.costKnown,
|
|
71
|
+
costSplit: costSplitOf(row),
|
|
57
72
|
},
|
|
58
73
|
};
|
|
59
74
|
}
|
|
@@ -225,11 +240,26 @@ export function createPostgresReadStore(source, tenantId) {
|
|
|
225
240
|
unpricedCalls: sql `count(*) filter (where ${modelUsage.costKnown} = false)`.mapWith(Number),
|
|
226
241
|
unreportedCalls: sql `count(*) filter (where ${modelUsage.tokensReported} = false)`.mapWith(Number),
|
|
227
242
|
unknownQualityCalls: sql `count(*) filter (where ${modelUsage.tokensReported} is null or ${modelUsage.costKnown} is null)`.mapWith(Number),
|
|
243
|
+
costInput: sum(modelUsage.costInputUsd),
|
|
244
|
+
costCacheRead: sum(modelUsage.costCacheReadUsd),
|
|
245
|
+
costCacheWrite: sum(modelUsage.costCacheWriteUsd),
|
|
246
|
+
costOutput: sum(modelUsage.costOutputUsd),
|
|
247
|
+
splitCalls: sql `count(${modelUsage.costInputUsd})`.mapWith(Number),
|
|
248
|
+
// The gate's timing is ISO text; a row timed before migration 0022 has none.
|
|
249
|
+
modelTimeMs: sql `coalesce(round(sum(extract(epoch from (${modelUsage.endedAt}::timestamptz - ${modelUsage.startedAt}::timestamptz)) * 1000)), 0)`.mapWith(Number),
|
|
250
|
+
timedCalls: sql `count(*) filter (where ${modelUsage.startedAt} is not null and ${modelUsage.endedAt} is not null)`.mapWith(Number),
|
|
228
251
|
})
|
|
229
252
|
.from(modelUsage)
|
|
230
253
|
.where(and(eq(modelUsage.sessionId, id), turnId === undefined ? undefined : eq(modelUsage.turnId, turnId)));
|
|
254
|
+
const { costInput, costCacheRead, costCacheWrite, costOutput, ...rest } = totals;
|
|
231
255
|
return {
|
|
232
|
-
...
|
|
256
|
+
...rest,
|
|
257
|
+
costSplit: {
|
|
258
|
+
input: costInput,
|
|
259
|
+
cacheRead: costCacheRead,
|
|
260
|
+
cacheWrite: costCacheWrite,
|
|
261
|
+
output: costOutput,
|
|
262
|
+
},
|
|
233
263
|
sessionId: id,
|
|
234
264
|
turnId: turnId ?? null,
|
|
235
265
|
asOf: new Date().toISOString(),
|
|
@@ -1793,7 +1793,7 @@ export declare const agentTriggers: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
1793
1793
|
}>;
|
|
1794
1794
|
/**
|
|
1795
1795
|
* An Agent's memories (track T5, D72): one row per Markdown file under `/memories`, its content
|
|
1796
|
-
* inline (the Runtime limit `memoryBytes`, 100
|
|
1796
|
+
* inline (the Runtime limit `memoryBytes`, 100 KiB by default, bounds it). `version` counts the
|
|
1797
1797
|
* changes to this path; `description` is its frontmatter's, for the pinned index. A write locks the
|
|
1798
1798
|
* one row it changes, so writes to different memories never wait on each other.
|
|
1799
1799
|
*/
|
|
@@ -2737,14 +2737,14 @@ export declare const vaultCredentials: import("drizzle-orm/pg-core").PgTableWith
|
|
|
2737
2737
|
tableName: "vault_credentials";
|
|
2738
2738
|
dataType: "string";
|
|
2739
2739
|
columnType: "PgText";
|
|
2740
|
-
data: "bearer" | "environment_secret" | "environment_variable" | "headers" | "model";
|
|
2740
|
+
data: "bearer" | "environment_secret" | "environment_variable" | "headers" | "model" | "workload";
|
|
2741
2741
|
driverParam: string;
|
|
2742
2742
|
notNull: true;
|
|
2743
2743
|
hasDefault: false;
|
|
2744
2744
|
isPrimaryKey: false;
|
|
2745
2745
|
isAutoincrement: false;
|
|
2746
2746
|
hasRuntimeDefault: false;
|
|
2747
|
-
enumValues: ["bearer", "headers", "environment_secret", "environment_variable", "model"];
|
|
2747
|
+
enumValues: ["bearer", "headers", "workload", "environment_secret", "environment_variable", "model"];
|
|
2748
2748
|
baseColumn: never;
|
|
2749
2749
|
identity: undefined;
|
|
2750
2750
|
generated: undefined;
|
|
@@ -3401,7 +3401,8 @@ export declare const signingKeys: import("drizzle-orm/pg-core").PgTableWithColum
|
|
|
3401
3401
|
* flagged `duplicate`; both were billed. Budgets read their spend from it. Token counts are 0
|
|
3402
3402
|
* when the provider sent none. `startedAt`, `endedAt` and `firstTokenMs` are the gate's own
|
|
3403
3403
|
* timing of the call (R7.1, migration 0022); null on rows written before, and `firstTokenMs`
|
|
3404
|
-
* when the provider streamed nothing.
|
|
3404
|
+
* when the provider streamed nothing. The `cost*Usd` columns split `costUsd` by token kind, at the
|
|
3405
|
+
* prices the adapter applied (migration 0037); null on rows written before.
|
|
3405
3406
|
*/
|
|
3406
3407
|
export declare const modelUsage: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
3407
3408
|
name: "model_usage";
|
|
@@ -3793,6 +3794,74 @@ export declare const modelUsage: import("drizzle-orm/pg-core").PgTableWithColumn
|
|
|
3793
3794
|
identity: undefined;
|
|
3794
3795
|
generated: undefined;
|
|
3795
3796
|
}, {}, {}>;
|
|
3797
|
+
costInputUsd: import("drizzle-orm/pg-core").PgColumn<{
|
|
3798
|
+
name: "costInputUsd";
|
|
3799
|
+
tableName: "model_usage";
|
|
3800
|
+
dataType: "number";
|
|
3801
|
+
columnType: "PgDoublePrecision";
|
|
3802
|
+
data: number;
|
|
3803
|
+
driverParam: string | number;
|
|
3804
|
+
notNull: false;
|
|
3805
|
+
hasDefault: false;
|
|
3806
|
+
isPrimaryKey: false;
|
|
3807
|
+
isAutoincrement: false;
|
|
3808
|
+
hasRuntimeDefault: false;
|
|
3809
|
+
enumValues: undefined;
|
|
3810
|
+
baseColumn: never;
|
|
3811
|
+
identity: undefined;
|
|
3812
|
+
generated: undefined;
|
|
3813
|
+
}, {}, {}>;
|
|
3814
|
+
costCacheReadUsd: import("drizzle-orm/pg-core").PgColumn<{
|
|
3815
|
+
name: "costCacheReadUsd";
|
|
3816
|
+
tableName: "model_usage";
|
|
3817
|
+
dataType: "number";
|
|
3818
|
+
columnType: "PgDoublePrecision";
|
|
3819
|
+
data: number;
|
|
3820
|
+
driverParam: string | number;
|
|
3821
|
+
notNull: false;
|
|
3822
|
+
hasDefault: false;
|
|
3823
|
+
isPrimaryKey: false;
|
|
3824
|
+
isAutoincrement: false;
|
|
3825
|
+
hasRuntimeDefault: false;
|
|
3826
|
+
enumValues: undefined;
|
|
3827
|
+
baseColumn: never;
|
|
3828
|
+
identity: undefined;
|
|
3829
|
+
generated: undefined;
|
|
3830
|
+
}, {}, {}>;
|
|
3831
|
+
costCacheWriteUsd: import("drizzle-orm/pg-core").PgColumn<{
|
|
3832
|
+
name: "costCacheWriteUsd";
|
|
3833
|
+
tableName: "model_usage";
|
|
3834
|
+
dataType: "number";
|
|
3835
|
+
columnType: "PgDoublePrecision";
|
|
3836
|
+
data: number;
|
|
3837
|
+
driverParam: string | number;
|
|
3838
|
+
notNull: false;
|
|
3839
|
+
hasDefault: false;
|
|
3840
|
+
isPrimaryKey: false;
|
|
3841
|
+
isAutoincrement: false;
|
|
3842
|
+
hasRuntimeDefault: false;
|
|
3843
|
+
enumValues: undefined;
|
|
3844
|
+
baseColumn: never;
|
|
3845
|
+
identity: undefined;
|
|
3846
|
+
generated: undefined;
|
|
3847
|
+
}, {}, {}>;
|
|
3848
|
+
costOutputUsd: import("drizzle-orm/pg-core").PgColumn<{
|
|
3849
|
+
name: "costOutputUsd";
|
|
3850
|
+
tableName: "model_usage";
|
|
3851
|
+
dataType: "number";
|
|
3852
|
+
columnType: "PgDoublePrecision";
|
|
3853
|
+
data: number;
|
|
3854
|
+
driverParam: string | number;
|
|
3855
|
+
notNull: false;
|
|
3856
|
+
hasDefault: false;
|
|
3857
|
+
isPrimaryKey: false;
|
|
3858
|
+
isAutoincrement: false;
|
|
3859
|
+
hasRuntimeDefault: false;
|
|
3860
|
+
enumValues: undefined;
|
|
3861
|
+
baseColumn: never;
|
|
3862
|
+
identity: undefined;
|
|
3863
|
+
generated: undefined;
|
|
3864
|
+
}, {}, {}>;
|
|
3796
3865
|
};
|
|
3797
3866
|
dialect: 'pg';
|
|
3798
3867
|
}>;
|
|
@@ -5656,10 +5725,14 @@ export type EnvironmentRowColumns = typeof agents.$inferSelect;
|
|
|
5656
5725
|
export type ArtifactRow = typeof artifacts.$inferSelect;
|
|
5657
5726
|
export type ArtifactVersionRow = typeof artifactVersions.$inferSelect;
|
|
5658
5727
|
export type DefinitionFileRow = typeof definitionFiles.$inferSelect;
|
|
5659
|
-
export type ModelUsageWrite = Omit<ModelUsageRow, "duplicate" | "txid" | "tokensReported" | "costKnown" | "startedAt" | "endedAt" | "firstTokenMs"> & {
|
|
5728
|
+
export type ModelUsageWrite = Omit<ModelUsageRow, "duplicate" | "txid" | "tokensReported" | "costKnown" | "startedAt" | "endedAt" | "firstTokenMs" | "costInputUsd" | "costCacheReadUsd" | "costCacheWriteUsd" | "costOutputUsd"> & {
|
|
5660
5729
|
tokensReported?: boolean | null;
|
|
5661
5730
|
costKnown?: boolean | null;
|
|
5662
5731
|
startedAt?: string | null;
|
|
5663
5732
|
endedAt?: string | null;
|
|
5664
5733
|
firstTokenMs?: number | null;
|
|
5734
|
+
costInputUsd?: number | null;
|
|
5735
|
+
costCacheReadUsd?: number | null;
|
|
5736
|
+
costCacheWriteUsd?: number | null;
|
|
5737
|
+
costOutputUsd?: number | null;
|
|
5665
5738
|
};
|
|
@@ -251,7 +251,7 @@ export const agentTriggers = nylorun.table("agent_triggers", {
|
|
|
251
251
|
]);
|
|
252
252
|
/**
|
|
253
253
|
* An Agent's memories (track T5, D72): one row per Markdown file under `/memories`, its content
|
|
254
|
-
* inline (the Runtime limit `memoryBytes`, 100
|
|
254
|
+
* inline (the Runtime limit `memoryBytes`, 100 KiB by default, bounds it). `version` counts the
|
|
255
255
|
* changes to this path; `description` is its frontmatter's, for the pinned index. A write locks the
|
|
256
256
|
* one row it changes, so writes to different memories never wait on each other.
|
|
257
257
|
*/
|
|
@@ -353,7 +353,7 @@ export const vaultCredentials = nylorun.table("vault_credentials", {
|
|
|
353
353
|
id: textC().primaryKey(),
|
|
354
354
|
vaultId: text().notNull(),
|
|
355
355
|
name: text().notNull(),
|
|
356
|
-
type: text({ enum: ["bearer", "headers", "environment_secret", "environment_variable", "model"] }).notNull(),
|
|
356
|
+
type: text({ enum: ["bearer", "headers", "workload", "environment_secret", "environment_variable", "model"] }).notNull(),
|
|
357
357
|
/** JSON text of the non-secret binding: url (and via, identity, header names), or provider and model. */
|
|
358
358
|
bindingJson: text().notNull(),
|
|
359
359
|
expiresAt: text(),
|
|
@@ -425,7 +425,8 @@ export const signingKeys = nylorun.table("signing_keys", {
|
|
|
425
425
|
* flagged `duplicate`; both were billed. Budgets read their spend from it. Token counts are 0
|
|
426
426
|
* when the provider sent none. `startedAt`, `endedAt` and `firstTokenMs` are the gate's own
|
|
427
427
|
* timing of the call (R7.1, migration 0022); null on rows written before, and `firstTokenMs`
|
|
428
|
-
* when the provider streamed nothing.
|
|
428
|
+
* when the provider streamed nothing. The `cost*Usd` columns split `costUsd` by token kind, at the
|
|
429
|
+
* prices the adapter applied (migration 0037); null on rows written before.
|
|
429
430
|
*/
|
|
430
431
|
export const modelUsage = nylorun.table("model_usage", {
|
|
431
432
|
id: textC().primaryKey(),
|
|
@@ -459,6 +460,14 @@ export const modelUsage = nylorun.table("model_usage", {
|
|
|
459
460
|
endedAt: text(),
|
|
460
461
|
/** From `startedAt` to the provider's first stream event. */
|
|
461
462
|
firstTokenMs: integer(),
|
|
463
|
+
/** What the uncached input cost. */
|
|
464
|
+
costInputUsd: doublePrecision(),
|
|
465
|
+
/** What the input read from the provider's cache cost. */
|
|
466
|
+
costCacheReadUsd: doublePrecision(),
|
|
467
|
+
/** What the input written to the provider's cache cost. */
|
|
468
|
+
costCacheWriteUsd: doublePrecision(),
|
|
469
|
+
/** What the output cost. */
|
|
470
|
+
costOutputUsd: doublePrecision(),
|
|
462
471
|
}, (t) => [
|
|
463
472
|
index("model_usage_session").on(t.sessionId, t.createdAt, t.id),
|
|
464
473
|
index("model_usage_export").on(t.txid, t.id),
|
package/dist/tenant/agents.js
CHANGED
|
@@ -562,15 +562,19 @@ async function parseSecretRequest(ctx, id, name, raw) {
|
|
|
562
562
|
const full = slotRequest(slot);
|
|
563
563
|
if (raw !== null && typeof raw === "object" && "type" in raw) {
|
|
564
564
|
const body = PutAgentSecretRequestSchema.parse(raw);
|
|
565
|
-
|
|
565
|
+
// The gateway's tool prefix is this installation's to add (`gateway-credentials`).
|
|
566
|
+
const { requestId: _, value: _v, headers: _h, toolPrefix: _p, ...binding } = body;
|
|
566
567
|
if (canonical(binding) !== canonical(full))
|
|
567
568
|
fail(409, `Secret ${name}'s type and binding come from Agent ${id}'s manifest: send only its value ({ "value": "…" }).`, {
|
|
568
569
|
code: "secret_not_in_manifest",
|
|
569
570
|
});
|
|
570
571
|
return body;
|
|
571
572
|
}
|
|
572
|
-
const { requestId, value, headers } = PutAgentSecretValueRequestSchema.parse(raw);
|
|
573
|
-
const withId =
|
|
573
|
+
const { requestId, value, headers, toolPrefix } = PutAgentSecretValueRequestSchema.parse(raw);
|
|
574
|
+
const withId = {
|
|
575
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
576
|
+
...(toolPrefix === undefined ? {} : { toolPrefix }),
|
|
577
|
+
};
|
|
574
578
|
if (slot.type === "headers") {
|
|
575
579
|
if (headers === undefined)
|
|
576
580
|
fail(400, `Secret ${name} is a headers credential: send { "headers": { … } }.`);
|
|
@@ -186,6 +186,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
|
|
|
186
186
|
code: string;
|
|
187
187
|
message: string;
|
|
188
188
|
server?: string | undefined;
|
|
189
|
+
gateway?: string | undefined;
|
|
189
190
|
retryable?: boolean | undefined;
|
|
190
191
|
};
|
|
191
192
|
output?: undefined;
|
|
@@ -208,6 +209,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
|
|
|
208
209
|
code: string;
|
|
209
210
|
message: string;
|
|
210
211
|
server?: string | undefined;
|
|
212
|
+
gateway?: string | undefined;
|
|
211
213
|
retryable?: boolean | undefined;
|
|
212
214
|
};
|
|
213
215
|
output?: undefined;
|
|
@@ -230,6 +232,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
|
|
|
230
232
|
code: string;
|
|
231
233
|
message: string;
|
|
232
234
|
server?: string | undefined;
|
|
235
|
+
gateway?: string | undefined;
|
|
233
236
|
retryable?: boolean | undefined;
|
|
234
237
|
};
|
|
235
238
|
output?: undefined;
|
|
@@ -252,6 +255,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
|
|
|
252
255
|
code: string;
|
|
253
256
|
message: string;
|
|
254
257
|
server?: string | undefined;
|
|
258
|
+
gateway?: string | undefined;
|
|
255
259
|
retryable?: boolean | undefined;
|
|
256
260
|
};
|
|
257
261
|
output?: undefined;
|
|
@@ -344,6 +348,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
|
|
|
344
348
|
code: string;
|
|
345
349
|
message: string;
|
|
346
350
|
server?: string | undefined;
|
|
351
|
+
gateway?: string | undefined;
|
|
347
352
|
retryable?: boolean | undefined;
|
|
348
353
|
};
|
|
349
354
|
output?: undefined;
|
|
@@ -368,6 +373,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
|
|
|
368
373
|
code: string;
|
|
369
374
|
message: string;
|
|
370
375
|
server?: string | undefined;
|
|
376
|
+
gateway?: string | undefined;
|
|
371
377
|
retryable?: boolean | undefined;
|
|
372
378
|
};
|
|
373
379
|
output?: undefined;
|
|
@@ -392,6 +398,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
|
|
|
392
398
|
code: string;
|
|
393
399
|
message: string;
|
|
394
400
|
server?: string | undefined;
|
|
401
|
+
gateway?: string | undefined;
|
|
395
402
|
retryable?: boolean | undefined;
|
|
396
403
|
};
|
|
397
404
|
output?: undefined;
|
|
@@ -416,6 +423,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
|
|
|
416
423
|
code: string;
|
|
417
424
|
message: string;
|
|
418
425
|
server?: string | undefined;
|
|
426
|
+
gateway?: string | undefined;
|
|
419
427
|
retryable?: boolean | undefined;
|
|
420
428
|
};
|
|
421
429
|
output?: undefined;
|
|
@@ -151,6 +151,8 @@ export function toolCompleted(request, value, facts) {
|
|
|
151
151
|
message: String(outcome.message ?? ""),
|
|
152
152
|
// `credential_rejected` (R2b C1): which server.
|
|
153
153
|
...(typeof outcome.server === "string" ? { server: outcome.server } : {}),
|
|
154
|
+
// And the gateway that refused the Runtime's workload token (#496).
|
|
155
|
+
...(typeof outcome.gateway === "string" ? { gateway: outcome.gateway } : {}),
|
|
154
156
|
...(typeof outcome.retryable === "boolean" ? { retryable: outcome.retryable } : {}),
|
|
155
157
|
},
|
|
156
158
|
}
|
package/dist/tenant/types.d.ts
CHANGED
|
@@ -74,6 +74,11 @@ export interface TenantConfig {
|
|
|
74
74
|
* the identity file (`NYLORUN_IDENTITY_FILE`). Absent: none.
|
|
75
75
|
*/
|
|
76
76
|
issuers?: import("./issuers.js").TrustedIssuers;
|
|
77
|
+
/**
|
|
78
|
+
* The URL clients reach the Runtime API at (`NYLORUN_PUBLIC_URL`, no trailing slash): the base
|
|
79
|
+
* of webhook URLs and the OAuth resource. Absent: the origin each request reached.
|
|
80
|
+
*/
|
|
81
|
+
publicUrl?: string;
|
|
77
82
|
logger: Logger;
|
|
78
83
|
}
|
|
79
84
|
export interface Logger {
|
package/dist/vault/headers.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** Headers that describe one hop and are never forwarded or set by a credential or a setting. */
|
|
2
|
+
export declare const HOP_BY_HOP_HEADERS: ReadonlySet<string>;
|
|
3
|
+
/**
|
|
4
|
+
* Why a custom model endpoint's header (`settings.headers`, `settings.callHeaders`, #493) may not
|
|
5
|
+
* be `name`, or undefined when it may: the API key's `authorization`, a hop-by-hop header, a
|
|
6
|
+
* transport header or `Nylorun-*`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function modelHeaderError(name: string): string | undefined;
|
|
1
9
|
/**
|
|
2
10
|
* The secret values of a credential's `headers`, as a server could echo them (R2b C8, Q16): each
|
|
3
11
|
* value of at least 8 characters, and the token of a `Bearer …` value. The identity header names
|
package/dist/vault/headers.js
CHANGED
|
@@ -18,6 +18,31 @@ const RESERVED_HEADERS = new Set([
|
|
|
18
18
|
"last-event-id",
|
|
19
19
|
"idempotency-key",
|
|
20
20
|
]);
|
|
21
|
+
/** Headers that describe one hop and are never forwarded or set by a credential or a setting. */
|
|
22
|
+
export const HOP_BY_HOP_HEADERS = new Set([
|
|
23
|
+
"connection",
|
|
24
|
+
"keep-alive",
|
|
25
|
+
"proxy-authenticate",
|
|
26
|
+
"proxy-authorization",
|
|
27
|
+
"proxy-connection",
|
|
28
|
+
"te",
|
|
29
|
+
"trailer",
|
|
30
|
+
"transfer-encoding",
|
|
31
|
+
"upgrade",
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Why a custom model endpoint's header (`settings.headers`, `settings.callHeaders`, #493) may not
|
|
35
|
+
* be `name`, or undefined when it may: the API key's `authorization`, a hop-by-hop header, a
|
|
36
|
+
* transport header or `Nylorun-*`.
|
|
37
|
+
*/
|
|
38
|
+
export function modelHeaderError(name) {
|
|
39
|
+
const lower = name.toLowerCase();
|
|
40
|
+
if (lower === "authorization")
|
|
41
|
+
return "A model header may not set authorization: the model's auth sends it";
|
|
42
|
+
if (HOP_BY_HOP_HEADERS.has(lower) || isReservedHeader(lower))
|
|
43
|
+
return `A model header may not set ${lower}`;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
21
46
|
/**
|
|
22
47
|
* The secret values of a credential's `headers`, as a server could echo them (R2b C8, Q16): each
|
|
23
48
|
* value of at least 8 characters, and the token of a `Bearer …` value. The identity header names
|
package/dist/vault/host-model.js
CHANGED
|
@@ -24,11 +24,14 @@ export class HostModelVault {
|
|
|
24
24
|
const payload = readHostPayload(this.kek(), row, binding);
|
|
25
25
|
const credential = binding.authType === "oauth"
|
|
26
26
|
? { type: "oauth", ...payload.oauth }
|
|
27
|
-
:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
: binding.authType === "workload"
|
|
28
|
+
? // The workload token is read from its file at each call (#496), never stored.
|
|
29
|
+
{ type: "workload" }
|
|
30
|
+
: {
|
|
31
|
+
type: "api_key",
|
|
32
|
+
key: payload.apiKey,
|
|
33
|
+
...(payload.env ? { env: payload.env } : {}),
|
|
34
|
+
};
|
|
32
35
|
return {
|
|
33
36
|
provider: binding.provider,
|
|
34
37
|
model: binding.model,
|
package/dist/vault/service.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CredentialCoverage, CredentialCoverageRequest, CredentialIdentity, EnvironmentSecretSlot, AgentSecretView, HostModelProviderInfo, HostModelView, PutAgentSecretRequest, PutHostModelRequest, CustomModelSettings, ModelInput, SelectHostModelRequest } from "@nylorun/core/contracts";
|
|
1
|
+
import type { CredentialCoverage, CredentialCoverageRequest, CredentialIdentity, EnvironmentSecretSlot, AgentSecretView, HostModelAuthType, HostModelProviderInfo, HostModelView, PutAgentSecretRequest, PutHostModelRequest, CustomModelSettings, ModelInput, SelectHostModelRequest } from "@nylorun/core/contracts";
|
|
2
2
|
import type { AgentManifest, WorkflowManifest } from "@nylorun/core/define";
|
|
3
3
|
import type { SessionStore, Tx, VaultCredentialRow } from "../store/types.js";
|
|
4
4
|
export declare const HOST_VAULT_ID = "host";
|
|
@@ -10,9 +10,9 @@ export type HostModelSecret = {
|
|
|
10
10
|
settings?: CustomModelSettings;
|
|
11
11
|
/** The inputs the operator added to catalog models, by `<provider>/<model>` (protocol 12). */
|
|
12
12
|
addedInputs?: Readonly<Record<string, readonly ModelInput[]>>;
|
|
13
|
-
authType:
|
|
13
|
+
authType: HostModelAuthType;
|
|
14
14
|
credential: {
|
|
15
|
-
type:
|
|
15
|
+
type: HostModelAuthType;
|
|
16
16
|
key?: string;
|
|
17
17
|
env?: Record<string, string>;
|
|
18
18
|
refresh?: string;
|
|
@@ -22,7 +22,7 @@ export type HostModelSecret = {
|
|
|
22
22
|
};
|
|
23
23
|
};
|
|
24
24
|
type UserCredentialRow = VaultCredentialRow & {
|
|
25
|
-
type: "bearer" | "headers" | "environment_secret" | "environment_variable";
|
|
25
|
+
type: "bearer" | "headers" | "workload" | "environment_secret" | "environment_variable";
|
|
26
26
|
};
|
|
27
27
|
/**
|
|
28
28
|
* A fork's secrets sealed for its new vault (`sealEnvironmentSecretCopies`): each copy's
|
|
@@ -54,6 +54,13 @@ export type AuthorizeResult = {
|
|
|
54
54
|
headers: Record<string, string>;
|
|
55
55
|
/** Where the request goes instead of `url` (a gateway), when the credential has one. */
|
|
56
56
|
via?: string;
|
|
57
|
+
/** How the gateway at `via` prefixes the server's tool names, when it does. */
|
|
58
|
+
toolPrefix?: string;
|
|
59
|
+
/**
|
|
60
|
+
* A `workload` credential (#496): `sessionCredentials` adds the Runtime's workload token as
|
|
61
|
+
* the bearer, on the gate side, where `via`'s origin is one the operator lists.
|
|
62
|
+
*/
|
|
63
|
+
workload?: true;
|
|
57
64
|
/** The header that names the session owner; `sessionCredentials` adds it. */
|
|
58
65
|
identity?: CredentialIdentity;
|
|
59
66
|
} | {
|
|
@@ -195,7 +202,7 @@ type ModelBinding = {
|
|
|
195
202
|
model: string;
|
|
196
203
|
baseUrl?: string;
|
|
197
204
|
settings?: CustomModelSettings;
|
|
198
|
-
authType:
|
|
205
|
+
authType: HostModelAuthType;
|
|
199
206
|
};
|
|
200
207
|
type HostSecretPayload = {
|
|
201
208
|
apiKey?: string;
|
package/dist/vault/service.js
CHANGED
|
@@ -83,7 +83,7 @@ import { canonical } from "../store/canonical.js";
|
|
|
83
83
|
import { decryptSecret, encryptSecret, VaultCryptoError } from "./crypto.js";
|
|
84
84
|
import { VaultError } from "./error.js";
|
|
85
85
|
import { credentialNeeds } from "./coverage.js";
|
|
86
|
-
import { checkCredentialHeaders } from "./headers.js";
|
|
86
|
+
import { checkCredentialHeaders, modelHeaderError } from "./headers.js";
|
|
87
87
|
import { hostModelCatalog, inputsOfHostModel } from "../model/catalog.js";
|
|
88
88
|
import { inputsError } from "../model/capabilities.js";
|
|
89
89
|
import { readAddedInputs } from "../tenant/model-setting.js";
|
|
@@ -247,7 +247,8 @@ export class VaultService {
|
|
|
247
247
|
({ payload, binding } = environmentCredential(name, body));
|
|
248
248
|
else {
|
|
249
249
|
const url = normalizeVaultUrl(body.url);
|
|
250
|
-
|
|
250
|
+
// A workload credential holds no value: the bearer is the Runtime's workload token (#496).
|
|
251
|
+
payload = body.type === "workload" ? {} : payloadOf(body);
|
|
251
252
|
binding = bindingOf(url, payload, body);
|
|
252
253
|
}
|
|
253
254
|
const kek = this.kek();
|
|
@@ -466,6 +467,8 @@ export class VaultService {
|
|
|
466
467
|
url,
|
|
467
468
|
headers,
|
|
468
469
|
...(binding.via === undefined ? {} : { via: binding.via }),
|
|
470
|
+
...(binding.toolPrefix === undefined ? {} : { toolPrefix: binding.toolPrefix }),
|
|
471
|
+
...(row.type === "workload" ? { workload: true } : {}),
|
|
469
472
|
...(binding.identity === undefined ? {} : { identity: binding.identity }),
|
|
470
473
|
};
|
|
471
474
|
}
|
|
@@ -541,15 +544,20 @@ export class VaultService {
|
|
|
541
544
|
const kek = this.kek();
|
|
542
545
|
return this.store.tx((t) => this.replay(t, `host-model:${body.idempotencyKey}`, body, async () => {
|
|
543
546
|
this.validateHostModel(body);
|
|
547
|
+
if (body.auth.type === "workload" && body.provider !== "custom")
|
|
548
|
+
throw new VaultError(400, "The workload token authenticates a custom provider only.");
|
|
544
549
|
await this.ensureHostVault(t);
|
|
545
550
|
const credentialId = hostModelCredentialId(body.provider);
|
|
546
551
|
await this.deleteHostProviderRows(t, body.provider);
|
|
552
|
+
// The workload token is read on every call and never stored (#496).
|
|
547
553
|
const payload = body.auth.type === "api_key"
|
|
548
554
|
? {
|
|
549
555
|
apiKey: body.auth.key,
|
|
550
556
|
...(body.auth.env ? { env: body.auth.env } : {}),
|
|
551
557
|
}
|
|
552
|
-
:
|
|
558
|
+
: body.auth.type === "oauth"
|
|
559
|
+
? { oauth: body.auth }
|
|
560
|
+
: {};
|
|
553
561
|
await this.insertHostCredential(t, kek, {
|
|
554
562
|
id: credentialId,
|
|
555
563
|
provider: body.provider,
|
|
@@ -673,6 +681,7 @@ export class VaultService {
|
|
|
673
681
|
}
|
|
674
682
|
if (!["http:", "https:"].includes(url.protocol))
|
|
675
683
|
throw new VaultError(400, "Base URL must be an HTTP(S) URL.");
|
|
684
|
+
checkModelHeaders(body.settings);
|
|
676
685
|
return;
|
|
677
686
|
}
|
|
678
687
|
if (body.baseUrl)
|
|
@@ -800,8 +809,9 @@ function conflictOf(type, binding, others) {
|
|
|
800
809
|
});
|
|
801
810
|
}
|
|
802
811
|
}
|
|
803
|
-
else if (type === "bearer" || type === "headers") {
|
|
804
|
-
const existing = others.find((row) => (row.type === "bearer" || row.type === "headers"
|
|
812
|
+
else if (type === "bearer" || type === "headers" || type === "workload") {
|
|
813
|
+
const existing = others.find((row) => (row.type === "bearer" || row.type === "headers" || row.type === "workload") &&
|
|
814
|
+
credentialBinding(row).url === binding.url);
|
|
805
815
|
if (existing)
|
|
806
816
|
throw new VaultError(409, `${existing.name} is already bound to ${binding.url}`, {
|
|
807
817
|
code: "credential_conflict",
|
|
@@ -828,6 +838,7 @@ function secretViewOf(row) {
|
|
|
828
838
|
: { url: binding.url },
|
|
829
839
|
...(binding.headerNames === undefined ? {} : { headerNames: binding.headerNames }),
|
|
830
840
|
...(binding.via === undefined ? {} : { via: binding.via }),
|
|
841
|
+
...(binding.toolPrefix === undefined ? {} : { toolPrefix: binding.toolPrefix }),
|
|
831
842
|
...(binding.identity === undefined ? {} : { identity: binding.identity }),
|
|
832
843
|
createdAt: row.createdAt,
|
|
833
844
|
...(row.rotatedAt ? { rotatedAt: row.rotatedAt } : {}),
|
|
@@ -909,6 +920,23 @@ export function readHostPayload(kek, row, binding) {
|
|
|
909
920
|
plaintext.fill(0);
|
|
910
921
|
}
|
|
911
922
|
}
|
|
923
|
+
/**
|
|
924
|
+
* A custom endpoint's `headers` and `callHeaders` (#493): no name the model's auth or the
|
|
925
|
+
* transport sets, and each name once across both maps, whatever its case.
|
|
926
|
+
*/
|
|
927
|
+
function checkModelHeaders(settings) {
|
|
928
|
+
const names = [...Object.keys(settings?.headers ?? {}), ...Object.keys(settings?.callHeaders ?? {})];
|
|
929
|
+
const seen = new Set();
|
|
930
|
+
for (const name of names) {
|
|
931
|
+
const refused = modelHeaderError(name);
|
|
932
|
+
if (refused)
|
|
933
|
+
throw new VaultError(400, refused);
|
|
934
|
+
const lower = name.toLowerCase();
|
|
935
|
+
if (seen.has(lower))
|
|
936
|
+
throw new VaultError(400, `The model header ${lower} is set twice`);
|
|
937
|
+
seen.add(lower);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
912
940
|
/** The sealed part of a tool credential: a token, or a header map with lower-cased names. */
|
|
913
941
|
function payloadOf(auth) {
|
|
914
942
|
if (auth.type === "bearer")
|
|
@@ -924,12 +952,16 @@ function bindingOf(url, payload, routing) {
|
|
|
924
952
|
return {
|
|
925
953
|
url,
|
|
926
954
|
...(routing.via ? { via: routing.via } : {}),
|
|
955
|
+
...(routing.via && routing.toolPrefix ? { toolPrefix: routing.toolPrefix } : {}),
|
|
927
956
|
...(routing.identity ? { identity: { header: routing.identity.header.toLowerCase() } } : {}),
|
|
928
957
|
...(payload.headers ? { headerNames: sent } : {}),
|
|
929
958
|
};
|
|
930
959
|
}
|
|
931
960
|
/** The headers a credential sends, or undefined when its payload holds none. */
|
|
932
961
|
function sendHeaders(row, payload) {
|
|
962
|
+
// Its bearer is the Runtime's workload token, which `sessionCredentials` adds.
|
|
963
|
+
if (row.type === "workload")
|
|
964
|
+
return {};
|
|
933
965
|
if (row.type === "headers")
|
|
934
966
|
return payload.headers && Object.keys(payload.headers).length > 0
|
|
935
967
|
? { ...payload.headers }
|
|
@@ -999,7 +1031,12 @@ function rebind(name, slot, row, payload, binding) {
|
|
|
999
1031
|
return undefined;
|
|
1000
1032
|
const sealed = payloadOf(auth);
|
|
1001
1033
|
const routing = slot;
|
|
1002
|
-
|
|
1034
|
+
// The gateway's tool prefix is the vault's, not the slot's: it stays while the slot keeps `via`.
|
|
1035
|
+
return {
|
|
1036
|
+
type,
|
|
1037
|
+
payload: sealed,
|
|
1038
|
+
binding: bindingOf(normalizeVaultUrl(routing.url), sealed, { ...routing, toolPrefix: binding.toolPrefix ?? null }),
|
|
1039
|
+
};
|
|
1003
1040
|
}
|
|
1004
1041
|
catch (error) {
|
|
1005
1042
|
if (error instanceof VaultError)
|
package/dist/vault/sources.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* and Tool Gate). The session comes from its row, never from the harness.
|
|
9
9
|
*/
|
|
10
10
|
import { isSubject } from "@nylorun/core/contracts";
|
|
11
|
+
import { workloadOriginRefused, workloadToken, WorkloadTokenUnavailable } from "./workload-token.js";
|
|
11
12
|
/**
|
|
12
13
|
* The credential of one request made for `session`: the answer of its Environment's vault
|
|
13
14
|
* (`VaultService.authorize`). A credential with an identity header (R2b C2)
|
|
@@ -16,11 +17,12 @@ import { isSubject } from "@nylorun/core/contracts";
|
|
|
16
17
|
* person (the reserved `installation`) sends none (Q3).
|
|
17
18
|
*/
|
|
18
19
|
export async function sessionCredentials(vault, session, request) {
|
|
19
|
-
const
|
|
20
|
+
const authorized = await vault.authorize({
|
|
20
21
|
sessionId: session.id,
|
|
21
22
|
...(session.environmentId === undefined ? {} : { environmentId: session.environmentId }),
|
|
22
23
|
url: request.url,
|
|
23
24
|
});
|
|
25
|
+
const result = authorized.status === "authorized" && authorized.workload ? await withWorkloadToken(authorized) : authorized;
|
|
24
26
|
if (result.status !== "authorized" || !result.identity || !isSubject(session.ownerUserId))
|
|
25
27
|
return result;
|
|
26
28
|
return {
|
|
@@ -28,3 +30,22 @@ export async function sessionCredentials(vault, session, request) {
|
|
|
28
30
|
headers: { ...result.headers, [result.identity.header]: session.ownerUserId },
|
|
29
31
|
};
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A `workload` credential's request with the Runtime's workload token as its bearer, read now
|
|
35
|
+
* (#496), or refused: when `via`'s origin is not one the operator lists, or the token cannot be
|
|
36
|
+
* read. Never sent without it.
|
|
37
|
+
*/
|
|
38
|
+
async function withWorkloadToken(result) {
|
|
39
|
+
const refused = (reason) => ({ status: "refused", url: result.url, credentialIds: [], reason });
|
|
40
|
+
const origin = result.via === undefined ? "a workload credential needs via" : workloadOriginRefused(result.via);
|
|
41
|
+
if (origin !== undefined)
|
|
42
|
+
return refused(origin);
|
|
43
|
+
try {
|
|
44
|
+
return { ...result, headers: { ...result.headers, authorization: `Bearer ${await workloadToken()}` } };
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof WorkloadTokenUnavailable)
|
|
48
|
+
return refused(error.message);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|