@nylorun/runtime 0.16.0-beta → 0.17.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +39 -27
  3. package/dist/admin-openapi.json +307 -7
  4. package/dist/api/ag-ui/endpoint.js +1 -2
  5. package/dist/api/ag-ui/routes.d.ts +1 -1
  6. package/dist/api/ag-ui/routes.js +10 -12
  7. package/dist/api/components.d.ts +54 -116
  8. package/dist/api/components.js +8 -11
  9. package/dist/api/http/app.js +2 -0
  10. package/dist/api/http/define.d.ts +9 -12
  11. package/dist/api/http/define.js +11 -26
  12. package/dist/api/http/routes/access.d.ts +3 -19
  13. package/dist/api/http/routes/access.js +12 -172
  14. package/dist/api/http/routes/actions.js +0 -1
  15. package/dist/api/http/routes/artifacts.js +1 -3
  16. package/dist/api/http/routes/me.d.ts +13 -0
  17. package/dist/api/http/routes/me.js +45 -0
  18. package/dist/api/http/routes/sandboxes.d.ts +2 -2
  19. package/dist/api/http/routes/sandboxes.js +1 -3
  20. package/dist/api/http/routes/sessions.js +6 -12
  21. package/dist/api/http/routes/vaults.d.ts +12 -3
  22. package/dist/api/http/routes/vaults.js +122 -54
  23. package/dist/api/openapi.js +4 -10
  24. package/dist/artifacts/links.js +6 -6
  25. package/dist/artifacts/service.js +1 -1
  26. package/dist/gates/mcp-handler.js +5 -1
  27. package/dist/gates/tenant-vaults.d.ts +18 -9
  28. package/dist/gates/tenant-vaults.js +17 -12
  29. package/dist/host/admin-api.d.ts +12 -5
  30. package/dist/host/admin-api.js +72 -6
  31. package/dist/host/app.d.ts +0 -1
  32. package/dist/host/app.js +13 -23
  33. package/dist/host/config-for.d.ts +4 -0
  34. package/dist/host/config-for.js +2 -0
  35. package/dist/host/config.d.ts +0 -6
  36. package/dist/host/create-host.d.ts +3 -10
  37. package/dist/host/create-host.js +4 -1
  38. package/dist/host/gates.js +6 -1
  39. package/dist/host/main.js +32 -10
  40. package/dist/host/stack-config.d.ts +11 -8
  41. package/dist/host/stack-config.js +23 -12
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +2 -0
  44. package/dist/keys/client.js +9 -1
  45. package/dist/keys/keys.d.ts +12 -2
  46. package/dist/keys/keys.js +8 -0
  47. package/dist/mcp/connect.js +2 -1
  48. package/dist/mcp/pool.d.ts +1 -0
  49. package/dist/mcp/pool.js +1 -0
  50. package/dist/openapi.json +2359 -4096
  51. package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
  52. package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
  53. package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
  54. package/dist/store/postgres/drizzle/meta/_journal.json +21 -0
  55. package/dist/store/postgres/schema.d.ts +194 -170
  56. package/dist/store/postgres/schema.js +46 -28
  57. package/dist/store/postgres/store.js +51 -100
  58. package/dist/store/postgres/tenant.d.ts +1 -1
  59. package/dist/store/postgres/tenant.js +2 -2
  60. package/dist/store/types.d.ts +24 -30
  61. package/dist/streams/control.d.ts +0 -5
  62. package/dist/streams/control.js +0 -8
  63. package/dist/streams/types.d.ts +2 -11
  64. package/dist/streams/types.js +1 -1
  65. package/dist/tenant/auth.d.ts +13 -9
  66. package/dist/tenant/auth.js +25 -32
  67. package/dist/tenant/commands.js +3 -7
  68. package/dist/tenant/context.d.ts +20 -22
  69. package/dist/tenant/delivery-token.js +4 -4
  70. package/dist/tenant/effects.d.ts +5 -1
  71. package/dist/tenant/effects.js +5 -16
  72. package/dist/tenant/ephemeral.d.ts +14 -2
  73. package/dist/tenant/ephemeral.js +8 -1
  74. package/dist/tenant/host-token.js +5 -5
  75. package/dist/tenant/identity-file.d.ts +75 -0
  76. package/dist/tenant/identity-file.js +202 -0
  77. package/dist/tenant/issuers.d.ts +48 -0
  78. package/dist/tenant/issuers.js +311 -0
  79. package/dist/tenant/jwt.js +3 -3
  80. package/dist/tenant/operator-keys.d.ts +18 -0
  81. package/dist/tenant/operator-keys.js +55 -0
  82. package/dist/tenant/outbound.d.ts +30 -0
  83. package/dist/tenant/outbound.js +142 -10
  84. package/dist/tenant/principals.d.ts +2 -11
  85. package/dist/tenant/principals.js +10 -30
  86. package/dist/tenant/run-token.js +3 -3
  87. package/dist/tenant/runtime.d.ts +4 -0
  88. package/dist/tenant/runtime.js +14 -1
  89. package/dist/tenant/sandboxes.d.ts +5 -5
  90. package/dist/tenant/sandboxes.js +5 -5
  91. package/dist/tenant/session-streams.d.ts +6 -10
  92. package/dist/tenant/session-streams.js +9 -27
  93. package/dist/tenant/sessions.d.ts +2 -2
  94. package/dist/tenant/sessions.js +1 -1
  95. package/dist/tenant/signing-keys.d.ts +2 -1
  96. package/dist/tenant/signing-keys.js +2 -2
  97. package/dist/tenant/streams.d.ts +2 -3
  98. package/dist/tenant/streams.js +1 -5
  99. package/dist/tenant/types.d.ts +21 -0
  100. package/dist/vault/crypto.d.ts +7 -0
  101. package/dist/vault/crypto.js +16 -0
  102. package/dist/vault/oauth.d.ts +55 -0
  103. package/dist/vault/oauth.js +186 -0
  104. package/dist/vault/service.d.ts +54 -7
  105. package/dist/vault/service.js +261 -17
  106. package/dist/vault/sources.d.ts +54 -0
  107. package/dist/vault/sources.js +185 -0
  108. package/dist/version.d.ts +1 -1
  109. package/dist/version.js +1 -1
  110. package/package.json +4 -3
  111. package/dist/host/cors.d.ts +0 -27
  112. package/dist/host/cors.js +0 -64
  113. package/dist/tenant/access-policy.d.ts +0 -30
  114. package/dist/tenant/access-policy.js +0 -52
  115. package/dist/tenant/browser.d.ts +0 -15
  116. package/dist/tenant/browser.js +0 -39
  117. package/dist/tenant/subject-limits.d.ts +0 -16
  118. package/dist/tenant/subject-limits.js +0 -45
  119. package/dist/tenant/tokens.d.ts +0 -14
  120. package/dist/tenant/tokens.js +0 -187
@@ -5,9 +5,8 @@
5
5
  * "snake_case"`, here, in `db.ts` and in `drizzle.config.ts`).
6
6
  *
7
7
  * - `nylorun` holds the Tenant's state: the one `tenant` row, the document tables, Action
8
- * endpoints, principals, vaults and credentials, signing keys, publishable keys, subject
9
- * epochs and usage, settings, the model usage ledger, the model budgets, the Tool Gate's
10
- * crossings and file artifacts with their versions.
8
+ * endpoints, principals, vaults and credentials, signing keys, settings, the model usage
9
+ * ledger, the model budgets, the Tool Gate's crossings and file artifacts with their versions.
11
10
  * - `nylorun_streams` holds the record (Durable Streams §6): `session_events`,
12
11
  * `session_log_heads` and the relay's `relay_slots`. The relay's publication is custom SQL
13
12
  * (`drizzle/0002_stream_relay.sql`).
@@ -160,11 +159,15 @@ export const vaults = nylorun.table("vaults", {
160
159
  /** JSON text, or null. */
161
160
  metadataJson: text(),
162
161
  createdAt: textC().notNull(),
163
- scope: text({ enum: ["user", "host"] }).notNull().default("user"),
162
+ /**
163
+ * `user`: one person's (owner `ownerUserId`); `installation`: the installation's own,
164
+ * attachable to any session (owner `installation`); `host`: the model vault, never attached.
165
+ */
166
+ scope: text({ enum: ["user", "installation", "host"] }).notNull().default("user"),
164
167
  }, (t) => [
165
168
  index("vaults_owner").on(t.ownerUserId, t.createdAt, t.id),
166
169
  uniqueIndex("vaults_one_host").on(t.scope).where(sql `scope = 'host'`),
167
- check("vaults_scope_check", sql `scope IN ('user', 'host')`),
170
+ check("vaults_scope_check", sql `scope IN ('user', 'installation', 'host')`),
168
171
  ]);
169
172
  /** A credential of a vault: its binding in the clear, its secret sealed. */
170
173
  export const vaultCredentials = nylorun.table("vault_credentials", {
@@ -207,11 +210,48 @@ export const vaultIdempotency = nylorun.table("vault_idempotency", {
207
210
  /** JSON text of the original response. */
208
211
  response: text().notNull(),
209
212
  });
213
+ /**
214
+ * An MCP OAuth connect between its start and its callback (F9 C2): found by the SHA-256 of its
215
+ * `state`, used once, for ten minutes. The PKCE verifier and any client secret are sealed under
216
+ * the vault key (`sealBytes`), which only the keys module holds.
217
+ */
218
+ export const oauthPending = nylorun.table("oauth_pending", {
219
+ stateHash: text().primaryKey(),
220
+ vaultId: text().notNull(),
221
+ /** The MCP server's name in the agent, the credential's name. */
222
+ server: text().notNull(),
223
+ /** The MCP server's URL (normalized): the credential's binding. */
224
+ url: text().notNull(),
225
+ tokenEndpoint: text().notNull(),
226
+ clientId: text().notNull(),
227
+ /** How the client authenticates at the token endpoint: `none`, `client_secret_basic` or `client_secret_post`. */
228
+ tokenEndpointAuth: text({ enum: ["none", "client_secret_basic", "client_secret_post"] }).notNull(),
229
+ /** The RFC 8707 resource indicator sent with the authorization request, if any. */
230
+ resource: text(),
231
+ kekId: text().notNull(),
232
+ /** Sealed; null for a public client. */
233
+ clientSecret: bytes(),
234
+ /** Sealed. */
235
+ codeVerifier: bytes().notNull(),
236
+ redirectUri: text().notNull(),
237
+ expiresAt: textC().notNull(),
238
+ createdAt: textC().notNull(),
239
+ }, (t) => [
240
+ foreignKey({
241
+ name: "oauth_pending_vault_id_fkey",
242
+ columns: [t.vaultId],
243
+ foreignColumns: [vaults.id],
244
+ }).onDelete("cascade"),
245
+ index("oauth_pending_expires").on(t.expiresAt),
246
+ ]);
210
247
  export const tenantSettings = nylorun.table("tenant_settings", {
211
248
  key: text().primaryKey(),
212
249
  value: text().notNull(),
213
250
  });
214
- /** The Tenant's signing keys (subject tokens): the private half sealed, the public JWK not. */
251
+ /**
252
+ * The Tenant's signing keys (delivery tokens, capability links, run and host tokens): the
253
+ * private half sealed, the public JWK not.
254
+ */
215
255
  export const signingKeys = nylorun.table("signing_keys", {
216
256
  id: textC().primaryKey(),
217
257
  state: text({ enum: ["standby", "current", "previous", "revoked"] }).notNull(),
@@ -233,28 +273,6 @@ export const signingKeys = nylorun.table("signing_keys", {
233
273
  uniqueIndex("signing_keys_one_current").on(t.state).where(sql `state = 'current'`),
234
274
  uniqueIndex("signing_keys_one_previous").on(t.state).where(sql `state = 'previous'`),
235
275
  ]);
236
- /** Each subject's revocation epoch. */
237
- export const subjectEpochs = nylorun.table("subject_epochs", {
238
- subject: textC().primaryKey(),
239
- epoch: bigint({ mode: "number" }).notNull(),
240
- revokedAt: text().notNull(),
241
- });
242
- /** Each subject's turn bucket: tokens left and when they were last refilled. */
243
- export const subjectUsage = nylorun.table("subject_usage", {
244
- subject: textC().primaryKey(),
245
- turnTokens: doublePrecision().notNull(),
246
- refilledAt: text().notNull(),
247
- });
248
- /** Publishable keys (Host feature `browser-access`): public by design. */
249
- export const publishableKeys = nylorun.table("publishable_keys", {
250
- id: textC().primaryKey(),
251
- key: textC().notNull().unique("publishable_keys_key_key"),
252
- name: text().notNull().unique("publishable_keys_name_key"),
253
- /** JSON array of allowed origins. */
254
- originsJson: text().notNull(),
255
- createdAt: text().notNull(),
256
- revokedAt: text(),
257
- });
258
276
  /**
259
277
  * Action endpoints: where the Runtime delivers each agent's Actions over HTTP, and the health
260
278
  * recent deliveries and the last ping report.
@@ -56,7 +56,7 @@ import { database, driverError } from "./db.js";
56
56
  import { expectedSchemaVersion, readSchemaVersion } from "./migrate.js";
57
57
  import { createPostgresRecordReader } from "./record.js";
58
58
  import { postgresRecordWriter, postgresSandboxRecordWriter } from "./record-writer.js";
59
- import { actions, artifacts, artifactVersions, artifactContent, commands, definitions, effects, endpoints, links, modelBudgets, modelUsage, principals, publishableKeys, sandboxes, sandboxEvents, sandboxResources, sessionEvents, sessionLogHeads, sessions, toJson, toolCrossings, signingKeys, subjectEpochs, subjectUsage, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, } from "./schema.js";
59
+ import { actions, artifacts, artifactVersions, artifactContent, commands, definitions, effects, endpoints, links, modelBudgets, modelUsage, principals, sandboxes, sandboxEvents, sandboxResources, sessionEvents, sessionLogHeads, sessions, toJson, toolCrossings, signingKeys, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, oauthPending, } from "./schema.js";
60
60
  export function createPostgresSessionStore(options) {
61
61
  return new PostgresSessionStore(options);
62
62
  }
@@ -760,6 +760,30 @@ class PostgresTx {
760
760
  .orderBy(principals.id);
761
761
  return rows.map((row) => row.tokenHash);
762
762
  }
763
+ async listPrincipals() {
764
+ this.check();
765
+ return await this.db.select().from(principals).orderBy(principals.id);
766
+ }
767
+ async putPrincipal(id, tokenHash, createdAt) {
768
+ this.check();
769
+ const [row] = await this.db
770
+ .insert(principals)
771
+ .values({ id, role: "application", tokenHash, idempotencyKey: null, createdAt })
772
+ .onConflictDoUpdate({
773
+ target: principals.id,
774
+ set: { tokenHash, createdAt, idempotencyKey: null },
775
+ })
776
+ .returning();
777
+ return row;
778
+ }
779
+ async deletePrincipal(id) {
780
+ this.check();
781
+ const rows = await this.db
782
+ .delete(principals)
783
+ .where(eq(principals.id, id))
784
+ .returning({ id: principals.id });
785
+ return rows.length > 0;
786
+ }
763
787
  // --- vault ---------------------------------------------------------------
764
788
  async insertVault(row) {
765
789
  this.check();
@@ -778,6 +802,14 @@ class PostgresTx {
778
802
  .where(and(eq(vaults.ownerUserId, ownerUserId), eq(vaults.scope, "user")))
779
803
  .orderBy(vaults.createdAt, vaults.id);
780
804
  }
805
+ async installationVaults() {
806
+ this.check();
807
+ return this.db
808
+ .select()
809
+ .from(vaults)
810
+ .where(eq(vaults.scope, "installation"))
811
+ .orderBy(vaults.createdAt, vaults.id);
812
+ }
781
813
  async updateVaultMetadata(id, metadataJson) {
782
814
  this.check();
783
815
  await this.db.update(vaults).set({ metadataJson }).where(eq(vaults.id, id));
@@ -858,7 +890,24 @@ class PostgresTx {
858
890
  this.check();
859
891
  await this.db.insert(vaultIdempotency).values(row);
860
892
  }
861
- // --- subject tokens ------------------------------------------------------
893
+ async insertOAuthPending(row) {
894
+ this.check();
895
+ await this.db.insert(oauthPending).values(row);
896
+ }
897
+ async takeOAuthPending(stateHash) {
898
+ this.check();
899
+ const [row] = await this.db
900
+ .delete(oauthPending)
901
+ .where(eq(oauthPending.stateHash, stateHash))
902
+ .returning();
903
+ return row;
904
+ }
905
+ async deleteExpiredOAuthPending(now) {
906
+ this.check();
907
+ const result = await this.db.delete(oauthPending).where(lt(oauthPending.expiresAt, now));
908
+ return result.count;
909
+ }
910
+ // --- signing keys --------------------------------------------------------
862
911
  async lockSigningKeys() {
863
912
  this.check();
864
913
  await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.signing_keys'))`);
@@ -895,103 +944,6 @@ class PostgresTx {
895
944
  const [row] = await this.db.select({ n: count() }).from(signingKeys);
896
945
  return row.n;
897
946
  }
898
- async subjectEpoch(subject) {
899
- this.check();
900
- const [row] = await this.db
901
- .select({ epoch: subjectEpochs.epoch })
902
- .from(subjectEpochs)
903
- .where(eq(subjectEpochs.subject, subject));
904
- return row?.epoch ?? 0;
905
- }
906
- async subjectEpochs(subjects) {
907
- this.check();
908
- if (subjects.length === 0)
909
- return new Map();
910
- const rows = await this.db
911
- .select({ subject: subjectEpochs.subject, epoch: subjectEpochs.epoch })
912
- .from(subjectEpochs)
913
- .where(inArray(subjectEpochs.subject, subjects));
914
- return new Map(rows.map((row) => [row.subject, row.epoch]));
915
- }
916
- async bumpSubjectEpoch(subject, at) {
917
- this.check();
918
- const [row] = await this.db
919
- .insert(subjectEpochs)
920
- .values({ subject, epoch: 1, revokedAt: at })
921
- .onConflictDoUpdate({
922
- target: subjectEpochs.subject,
923
- set: { epoch: sql `${subjectEpochs.epoch} + 1`, revokedAt: excluded("revoked_at") },
924
- })
925
- .returning({ epoch: subjectEpochs.epoch });
926
- return row.epoch;
927
- }
928
- async lockSubjectUsage(initial) {
929
- this.check();
930
- await this.db.insert(subjectUsage).values(initial).onConflictDoNothing();
931
- const [row] = await this.db
932
- .select()
933
- .from(subjectUsage)
934
- .where(eq(subjectUsage.subject, initial.subject))
935
- .for("update");
936
- return row;
937
- }
938
- async putSubjectUsage(row) {
939
- this.check();
940
- await this.db
941
- .insert(subjectUsage)
942
- .values(row)
943
- .onConflictDoUpdate({
944
- target: subjectUsage.subject,
945
- set: { turnTokens: excluded("turn_tokens"), refilledAt: excluded("refilled_at") },
946
- });
947
- }
948
- async countOwnerSessions(ownerUserId, statuses) {
949
- this.check();
950
- const [row] = await this.db
951
- .select({ n: count() })
952
- .from(sessions)
953
- .where(and(eq(sessions.ownerUserId, ownerUserId), inArray(sessions.status, statuses)));
954
- return row.n;
955
- }
956
- // --- publishable keys ----------------------------------------------------
957
- async insertPublishableKey(row) {
958
- this.check();
959
- await this.db.insert(publishableKeys).values(row);
960
- }
961
- async publishableKeyByKey(key) {
962
- this.check();
963
- const [row] = await this.db
964
- .select()
965
- .from(publishableKeys)
966
- .where(eq(publishableKeys.key, key));
967
- return row;
968
- }
969
- async publishableKey(id) {
970
- this.check();
971
- const [row] = await this.db
972
- .select()
973
- .from(publishableKeys)
974
- .where(eq(publishableKeys.id, id));
975
- return row;
976
- }
977
- async publishableKeys() {
978
- this.check();
979
- return this.db
980
- .select()
981
- .from(publishableKeys)
982
- .orderBy(publishableKeys.createdAt, publishableKeys.id);
983
- }
984
- async updatePublishableKey(id, patch) {
985
- this.check();
986
- if (patch.originsJson === undefined && patch.revokedAt === undefined)
987
- return (await this.publishableKey(id)) !== undefined;
988
- const rows = await this.db
989
- .update(publishableKeys)
990
- .set(patch)
991
- .where(eq(publishableKeys.id, id))
992
- .returning({ id: publishableKeys.id });
993
- return rows.length === 1;
994
- }
995
947
  // --- model usage ---------------------------------------------------------
996
948
  async recordModelUsage(row) {
997
949
  this.check();
@@ -1208,7 +1160,6 @@ class PostgresTx {
1208
1160
  if (scope === "sessions" || scope === "all") {
1209
1161
  for (const table of SESSION_TABLES)
1210
1162
  await db.delete(table);
1211
- await db.delete(subjectUsage);
1212
1163
  await db.delete(toolCrossings);
1213
1164
  // A session's artifacts go with it (their versions by cascade).
1214
1165
  await db.delete(artifacts).where(isNotNull(artifacts.sessionId));
@@ -16,7 +16,7 @@ export interface TenantCreation {
16
16
  /** Ignored when the Tenant exists. */
17
17
  name: string;
18
18
  /**
19
- * The principals the Tenant must have, given its id (derived keys depend on it). Created
19
+ * The principals the Tenant must have, given its id (Studio's derived key depends on it). Created
20
20
  * with the Tenant; on later opens, the missing ones are added.
21
21
  */
22
22
  principals?(tenantId: string): readonly InitialPrincipal[];
@@ -14,8 +14,8 @@
14
14
  * 3. When `nylorun.tenant` is empty, create the Tenant: its row (id, name, created time) and
15
15
  * its first principals. A database whose row is gone but that holds a Tenant's data
16
16
  * (principals, sessions, agents, vaults, keys) is `envelope-invalid`, never given a new
17
- * Tenant. Principals given later that the Tenant does not have yet are added (a derived
18
- * principal configured after the first start); existing ones are kept.
17
+ * Tenant. Principals given later that the Tenant does not have yet are added (the
18
+ * ephemeral Runtime's application key); existing ones are kept, as ordinary keys.
19
19
  * 4. Read the envelope back (`envelope-invalid` when it does not parse), and open the store.
20
20
  *
21
21
  * A lost connection or an unavailable server says nothing about the Tenant: it is thrown as
@@ -58,7 +58,7 @@ import type { RecordReader } from "../streams/relay/types.js";
58
58
  import type { Action, EventPayload, EventType, LiveEvent, SandboxEvent, SandboxEventPayload, SandboxEventType, SandboxKind, SandboxPodDesired, SandboxPodObserved, SessionEventOf } from "@nylorun/core/contracts";
59
59
  import type { SandboxManifest } from "@nylorun/core/define";
60
60
  import type { HostEffect } from "@nylorun/harness/run";
61
- import type { ArtifactRow, ArtifactVersionRow, ModelBudgetRow, ModelUsageRow, PrincipalRow, ToolCrossingRow, PublishableKeyRow, SigningKeyRow, SubjectUsageRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow } from "./postgres/schema.js";
61
+ import type { ArtifactRow, ArtifactVersionRow, ModelBudgetRow, ModelUsageRow, PrincipalRow, ToolCrossingRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow, OAuthPendingRow } from "./postgres/schema.js";
62
62
  /** Tables holding JSON documents keyed by `id`. */
63
63
  export type DocTable = "definitions" | "sessions" | "commands" | "effects" | "actions" | "sandboxes" | "links";
64
64
  export declare const DOC_TABLES: readonly DocTable[];
@@ -242,10 +242,9 @@ export type EndpointHealthUpdate =
242
242
  * The rows of the typed tables, inferred from the tables Drizzle defines
243
243
  * (`store/postgres/schema.ts`): a principal, a vault and its credentials (secrets sealed in
244
244
  * `bytea` columns, never inside a JSON body), the vault's audit and idempotency records, the
245
- * Tenant's signing keys, a subject's turn bucket, a publishable key, a model call in the usage
246
- * ledger and a model budget.
245
+ * Tenant's signing keys, a model call in the usage ledger and a model budget.
247
246
  */
248
- export type { ArtifactRow, ArtifactVersionRow, ModelBudgetRow, ModelUsageRow, ToolCrossingRow, PrincipalRow, PublishableKeyRow, SigningKeyRow, SubjectUsageRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow, };
247
+ export type { ArtifactRow, ArtifactVersionRow, ModelBudgetRow, ModelUsageRow, ToolCrossingRow, PrincipalRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow, OAuthPendingRow, };
249
248
  /** Which rows of the usage ledger a total covers. */
250
249
  export interface ModelUsageQuery {
251
250
  scope: "tenant" | "agent" | "turn";
@@ -480,11 +479,23 @@ export interface Tx {
480
479
  principalByTokenHash(tokenHash: string): Promise<PrincipalRow | undefined>;
481
480
  principalById(id: string): Promise<PrincipalRow | undefined>;
482
481
  applicationTokenHashes(): Promise<string[]>;
482
+ /** Every principal, ordered by id. */
483
+ listPrincipals(): Promise<PrincipalRow[]>;
484
+ /**
485
+ * Creates application principal `id` with `tokenHash`, or gives an existing one this hash
486
+ * (and `createdAt`): its previous key stops authenticating. Rejects when another principal
487
+ * holds the hash.
488
+ */
489
+ putPrincipal(id: string, tokenHash: string, createdAt: string): Promise<PrincipalRow>;
490
+ /** Deletes principal `id`; false when there was none. */
491
+ deletePrincipal(id: string): Promise<boolean>;
483
492
  /** Rejects on a duplicate id, or a second `host` vault. */
484
493
  insertVault(row: VaultRow): Promise<void>;
485
494
  getVault(id: string): Promise<VaultRow | undefined>;
486
495
  /** User vaults of one owner, ordered by `createdAt`, then id. */
487
496
  vaultsByOwner(ownerUserId: string): Promise<VaultRow[]>;
497
+ /** Installation vaults (any session may attach them), ordered by `createdAt`, then id. */
498
+ installationVaults(): Promise<VaultRow[]>;
488
499
  updateVaultMetadata(id: string, metadataJson: string | null): Promise<void>;
489
500
  /** Deletes the vault and its credentials. */
490
501
  deleteVault(id: string): Promise<void>;
@@ -510,6 +521,12 @@ export interface Tx {
510
521
  getVaultIdempotency(id: string): Promise<VaultIdempotencyRow | undefined>;
511
522
  /** Rejects when the id exists. */
512
523
  insertVaultIdempotency(row: VaultIdempotencyRow): Promise<void>;
524
+ /** An MCP OAuth connect's start (F9 C2). Rejects on a duplicate state or an unknown vault. */
525
+ insertOAuthPending(row: OAuthPendingRow): Promise<void>;
526
+ /** Deletes and returns the pending connect of `stateHash`, expired or not: each is used once. */
527
+ takeOAuthPending(stateHash: string): Promise<OAuthPendingRow | undefined>;
528
+ /** Deletes the pending connects that expired before `now` (ISO); returns how many. */
529
+ deleteExpiredOAuthPending(now: string): Promise<number>;
513
530
  /**
514
531
  * Serializes signing key changes: held until the transaction ends. Take it before reading
515
532
  * the keys a change depends on, so concurrent first uses or rotations do not collide.
@@ -527,28 +544,6 @@ export interface Tx {
527
544
  */
528
545
  setSigningKeyState(id: string, from: SigningKeyRow["state"], to: SigningKeyRow["state"], at: string): Promise<boolean>;
529
546
  countSigningKeys(): Promise<number>;
530
- /** The subject's revocation epoch; 0 when it was never revoked. */
531
- subjectEpoch(subject: string): Promise<number>;
532
- /** The epochs of several subjects; subjects never revoked are absent. */
533
- subjectEpochs(subjects: readonly string[]): Promise<Map<string, number>>;
534
- /** Adds one to the subject's epoch and returns the new value. */
535
- bumpSubjectEpoch(subject: string, at: string): Promise<number>;
536
- /**
537
- * The subject's turn bucket, created from `initial` when missing, locked until the
538
- * transaction ends so concurrent commands of one subject serialize.
539
- */
540
- lockSubjectUsage(initial: SubjectUsageRow): Promise<SubjectUsageRow>;
541
- putSubjectUsage(row: SubjectUsageRow): Promise<void>;
542
- /** How many sessions of `ownerUserId` are in one of `statuses`. */
543
- countOwnerSessions(ownerUserId: string, statuses: readonly string[]): Promise<number>;
544
- /** Rejects on a duplicate id, key or name. */
545
- insertPublishableKey(row: PublishableKeyRow): Promise<void>;
546
- publishableKeyByKey(key: string): Promise<PublishableKeyRow | undefined>;
547
- publishableKey(id: string): Promise<PublishableKeyRow | undefined>;
548
- /** All keys, revoked ones included, ordered by `createdAt`, then id. */
549
- publishableKeys(): Promise<PublishableKeyRow[]>;
550
- /** Returns false when the key does not exist. */
551
- updatePublishableKey(id: string, patch: Partial<Pick<PublishableKeyRow, "originsJson" | "revokedAt">>): Promise<boolean>;
552
547
  /** Appends a row, setting `duplicate` when one with the same `effectKey` exists; returns it. */
553
548
  recordModelUsage(row: Omit<ModelUsageRow, "duplicate">): Promise<ModelUsageRow>;
554
549
  modelUsageTotals(query: ModelUsageQuery): Promise<ModelUsageTotals>;
@@ -616,15 +611,14 @@ export interface Tx {
616
611
  putSetting(key: string, value: string): Promise<void>;
617
612
  /**
618
613
  * Deletes Tenant state by scope, in this transaction:
619
- * - `sessions`: sessions, commands, effects, actions, links, subject turn
620
- * buckets, the artifacts of sessions (their blobs are the caller's to delete) and the
614
+ * - `sessions`: sessions, commands, effects, actions, links, the artifacts of sessions (their blobs are the caller's to delete) and the
621
615
  * Tenant's record rows and log heads. The Tenant moves to the next basin
622
616
  * generation and the current one is retired, so session ids it frees start again in an
623
617
  * empty basin;
624
618
  * - `sandboxes`: sandbox records, sandbox resources and their lifecycle streams;
625
619
  * - `all`: both, plus definitions, Action endpoints, user vaults with their credentials,
626
- * the model usage ledger, the model budgets and Tenant-wide artifacts. The host vault, principals, signing keys, subject epochs,
627
- * publishable keys, settings, audit and vault idempotency rows stay.
620
+ * the model usage ledger, the model budgets and Tenant-wide artifacts. The host vault, principals, signing keys,
621
+ * settings, audit and vault idempotency rows stay.
628
622
  */
629
623
  reset(scope: ResetScope): Promise<void>;
630
624
  }
@@ -13,8 +13,3 @@ export declare function signalActionResolved(streams: DurableStreams, basin: str
13
13
  * Tenant open moves its readers to `generation` and ends the streams of deleted sessions.
14
14
  */
15
15
  export declare function signalSessionsReset(streams: DurableStreams, basin: string, generation: number): Promise<void>;
16
- /**
17
- * Appends a `subject.revoked` signal: every process with the Tenant open ends the subject's
18
- * streams opened with a token older than `epoch`.
19
- */
20
- export declare function signalSubjectRevoked(streams: DurableStreams, basin: string, subject: string, epoch: number): Promise<void>;
@@ -28,11 +28,3 @@ export async function signalSessionsReset(streams, basin, generation) {
28
28
  const signal = { type: "sessions.reset", generation };
29
29
  await streams.append(basin, CONTROL_STREAM, [signal]);
30
30
  }
31
- /**
32
- * Appends a `subject.revoked` signal: every process with the Tenant open ends the subject's
33
- * streams opened with a token older than `epoch`.
34
- */
35
- export async function signalSubjectRevoked(streams, basin, subject, epoch) {
36
- const signal = { type: "subject.revoked", subject, epoch };
37
- await streams.append(basin, CONTROL_STREAM, [signal]);
38
- }
@@ -7,7 +7,7 @@
7
7
  * | Stream | Contents | Written by | Read by |
8
8
  * | --- | --- | --- | --- |
9
9
  * | `sessions/<sessionId>` | every event of the session, in sequence | the stream relay, from the record | history and session SSE |
10
- * | `tenant/control` | `session.cancel`, `sessions.reset` and `subject.revoked` signals | API nodes | every process with the Tenant open |
10
+ * | `tenant/control` | `session.cancel`, `sessions.reset` and `action.resolved` signals | API nodes | every process with the Tenant open |
11
11
  *
12
12
  * **Basin generations.** A session id is unique within its Tenant's basin generation. A
13
13
  * Tenant reset, the only path that frees ids, moves the Tenant to the next generation, so
@@ -138,15 +138,6 @@ export interface SessionsResetSignal {
138
138
  type: "sessions.reset";
139
139
  generation?: number;
140
140
  }
141
- /**
142
- * A subject's tokens older than `epoch` were revoked: each process ends that subject's
143
- * streams opened with them.
144
- */
145
- export interface SubjectRevokedSignal {
146
- type: "subject.revoked";
147
- subject: string;
148
- epoch: number;
149
- }
150
141
  /**
151
142
  * An Action of `sessionId` has its outcome (F6.2): the process whose harness holds the
152
143
  * session's run while the Action is pending passes it on (`effect.resolved`).
@@ -156,4 +147,4 @@ export interface ActionResolvedSignal {
156
147
  sessionId: string;
157
148
  actionId: string;
158
149
  }
159
- export type ControlSignal = SessionCancelSignal | SessionsResetSignal | SubjectRevokedSignal | ActionResolvedSignal;
150
+ export type ControlSignal = SessionCancelSignal | SessionsResetSignal | ActionResolvedSignal;
@@ -7,7 +7,7 @@
7
7
  * | Stream | Contents | Written by | Read by |
8
8
  * | --- | --- | --- | --- |
9
9
  * | `sessions/<sessionId>` | every event of the session, in sequence | the stream relay, from the record | history and session SSE |
10
- * | `tenant/control` | `session.cancel`, `sessions.reset` and `subject.revoked` signals | API nodes | every process with the Tenant open |
10
+ * | `tenant/control` | `session.cancel`, `sessions.reset` and `action.resolved` signals | API nodes | every process with the Tenant open |
11
11
  *
12
12
  * **Basin generations.** A session id is unique within its Tenant's basin generation. A
13
13
  * Tenant reset, the only path that frees ids, moves the Tenant to the next generation, so
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * Tenant request authentication and authorization: a bearer token resolves to an application
3
- * principal, a subject token or a delivery token; anything else is the opaque 404 (D5).
4
- * An application principal may act for a subject (`Nylorun-Subject`, `Nylorun-Scopes`); a
5
- * subject token names its subject itself. `authorize` limits both to the routes their scopes
6
- * allow, decided from the route alone.
3
+ * principal, a trusted issuer's token or a delivery token; anything else is the opaque 404
4
+ * (D5). An application principal may act for a subject (`Nylorun-Subject`, `Nylorun-Scopes`);
5
+ * an issuer token names its subject itself. `requireScopes` limits them to the routes their
6
+ * scopes allow, decided from the route alone.
7
+ *
8
+ * Browsers: a request with `Origin` may carry an issuer token (CORS is the operator's proxy's),
9
+ * never an application key or a delivery token, which are server secrets.
7
10
  */
8
11
  import type { IncomingMessage } from "node:http";
9
12
  import { type Action, type SubjectScope } from "@nylorun/core/contracts";
10
13
  import type { AuthScope, SessionAccess, TenantContext } from "./context.js";
11
- import type { BrowserClient } from "./browser.js";
12
- export declare function authenticate(ctx: TenantContext, request: IncomingMessage, client?: BrowserClient): Promise<AuthScope>;
14
+ export declare function authenticate(ctx: TenantContext, request: IncomingMessage): Promise<AuthScope>;
13
15
  /**
14
16
  * What a subject needs for a route: any one of the scopes, nothing grants it (`never`), or
15
17
  * any caller may (`any`, public data such as the JWKS). Each route declares it
@@ -17,12 +19,12 @@ export declare function authenticate(ctx: TenantContext, request: IncomingMessag
17
19
  */
18
20
  export type SubjectAccess = readonly SubjectScope[] | "never" | "any";
19
21
  /**
20
- * A subject, subject token or publishable key reaches a route only with one of the scopes it
21
- * declares: `403 scope_required`, decided from the route alone before anything is read.
22
+ * A subject or a token caller reaches a route only with one of the scopes it declares:
23
+ * `403 scope_required`, decided from the route alone before anything is read.
22
24
  */
23
25
  export declare function requireScopes(scope: AuthScope, access: SubjectAccess): void;
24
26
  export declare function accessOf(scope: AuthScope): SessionAccess | undefined;
25
- /** The subject whose sessions and vaults the request is limited to, if it acts for one. */
27
+ /** The subject whose sessions the request is limited to, if it acts for one. */
26
28
  export declare function ownerOf(scope: AuthScope): string | undefined;
27
29
  /** The application principal behind the request, acting for a subject or not. */
28
30
  export declare function requirePrincipal(scope: AuthScope): string;
@@ -31,4 +33,6 @@ export declare function requirePrincipal(scope: AuthScope): string;
31
33
  * is still the Action's is checked by each callback.
32
34
  */
33
35
  export declare function scoped(scope: AuthScope, action: Action): void;
36
+ /** True when a token caller may reach `agentId` (its issuer's allowlist). Other callers always may. */
37
+ export declare function mayUseAgent(scope: AuthScope, agentId: string): boolean;
34
38
  export declare function requireApplication(scope: AuthScope): string;