@pstdio/pocketcoder-cli 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -109,12 +109,12 @@ var require_main = __commonJS((exports, module) => {
109
109
  err.code = "MISSING_DATA";
110
110
  throw err;
111
111
  }
112
- const keys = _dotenvKey(options).split(",");
113
- const length = keys.length;
112
+ const keys2 = _dotenvKey(options).split(",");
113
+ const length = keys2.length;
114
114
  let decrypted;
115
115
  for (let i = 0;i < length; i++) {
116
116
  try {
117
- const key = keys[i].trim();
117
+ const key = keys2[i].trim();
118
118
  const attrs = _instructions(result, key);
119
119
  decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
120
120
  break;
@@ -5214,7 +5214,7 @@ function isYargsInstance(y) {
5214
5214
  var Yargs = YargsFactory(esm_default);
5215
5215
  var yargs_default = Yargs;
5216
5216
  // package.json
5217
- var version = "0.7.3";
5217
+ var version = "0.8.0";
5218
5218
 
5219
5219
  // src/command/cli-context.ts
5220
5220
  import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
@@ -21235,6 +21235,20 @@ class OperationCapacityExceededError extends Error {
21235
21235
  var TEMPLATE_STATUSES = ["active", "available", "retired"];
21236
21236
  // ../runtime-contracts/src/stores/warm-pools.ts
21237
21237
  var WARM_POOL_RUNTIME_STATES = ["provisioning", "ready", "leasing", "leased", "draining", "failed"];
21238
+ // ../runtime-contracts/src/stores/workspaces.ts
21239
+ function purgedContentPatch() {
21240
+ return {
21241
+ launchInput: null,
21242
+ health: {},
21243
+ outputs: {},
21244
+ failureLogTail: null,
21245
+ failureLogTailTruncated: false,
21246
+ failureLastLogSeq: null,
21247
+ resolvedSource: null,
21248
+ registrationDigest: null,
21249
+ reconnectDigest: null
21250
+ };
21251
+ }
21238
21252
  // ../../node_modules/.bun/drizzle-orm@1.0.0-rc.4+b8545101ea3de3ba/node_modules/drizzle-orm/sql/functions/aggregate.js
21239
21253
  function count2(expression) {
21240
21254
  return sql`count(${expression || sql.raw("*")})`.mapWith(Number);
@@ -21284,6 +21298,9 @@ function createAccessTables(table2 = pgTable) {
21284
21298
  createdAt: timestamptz("created_at").notNull()
21285
21299
  });
21286
21300
  const machineKeys = table2("machine_keys", {
21301
+ issuanceRequestId: text2("issuance_request_id"),
21302
+ issuanceRequestDigest: text2("issuance_request_digest"),
21303
+ managedPrincipalIds: text2("managed_principal_ids").array().notNull().default([]),
21287
21304
  id: uuid("id").primaryKey(),
21288
21305
  principalId: uuid("principal_id").notNull().references(() => principals.id),
21289
21306
  secretDigest: bytea2("secret_digest").$type().notNull(),
@@ -21292,7 +21309,7 @@ function createAccessTables(table2 = pgTable) {
21292
21309
  expiresAt: timestamptz("expires_at"),
21293
21310
  revokedAt: timestamptz("revoked_at"),
21294
21311
  lastUsedAt: timestamptz("last_used_at")
21295
- });
21312
+ }, (table3) => [unique().on(table3.principalId, table3.issuanceRequestId)]);
21296
21313
  return { templates: templates2, principals, machineKeys };
21297
21314
  }
21298
21315
 
@@ -35640,6 +35657,58 @@ var AttachmentResolvedPayload = exports_external.object({
35640
35657
  descriptors: exports_external.array(AttachmentDescriptorSchema).optional(),
35641
35658
  missing_id: exports_external.uuid().optional()
35642
35659
  });
35660
+ // ../contracts/src/common/scopes.ts
35661
+ var SCOPES = [
35662
+ "templates:read",
35663
+ "workspaces:create",
35664
+ "workspaces:read",
35665
+ "workspaces:cancel",
35666
+ "workspaces:purge",
35667
+ "workspaces:recover",
35668
+ "keys:read",
35669
+ "keys:write",
35670
+ "workspaces:preserve",
35671
+ "workspaces:restore",
35672
+ "checkpoints:read",
35673
+ "checkpoints:delete",
35674
+ "outputs:read",
35675
+ "conversations:read",
35676
+ "conversations:delete",
35677
+ "services:relay",
35678
+ "attachments:write",
35679
+ "logs:read",
35680
+ "network:read",
35681
+ "terminal:attach",
35682
+ "terminal:read",
35683
+ "admin"
35684
+ ];
35685
+ function isScope(value) {
35686
+ return SCOPES.includes(value);
35687
+ }
35688
+ function hasScope(granted, required2) {
35689
+ return granted.includes(required2) || granted.includes("admin");
35690
+ }
35691
+
35692
+ // ../contracts/src/auth/keys.ts
35693
+ var KeyIssueRequestSchema = exports_external.strictObject({
35694
+ request_id: exports_external.string().min(1).max(128).regex(/^[A-Za-z0-9._:-]+$/),
35695
+ scopes: exports_external.array(exports_external.enum(SCOPES)).min(1).max(SCOPES.length),
35696
+ expires_at: exports_external.iso.datetime()
35697
+ });
35698
+ var KeyResourceSchema = exports_external.object({
35699
+ id: exports_external.uuid(),
35700
+ principal_id: exports_external.uuid(),
35701
+ scopes: exports_external.array(exports_external.string()),
35702
+ effective_scopes: exports_external.array(exports_external.string()),
35703
+ managed_principal_ids: exports_external.array(exports_external.uuid()),
35704
+ issuance_request_id: exports_external.string().nullable(),
35705
+ created_at: exports_external.iso.datetime(),
35706
+ expires_at: exports_external.iso.datetime().nullable(),
35707
+ revoked_at: exports_external.iso.datetime().nullable(),
35708
+ last_used_at: exports_external.iso.datetime().nullable()
35709
+ });
35710
+ var KeyIssueResponseSchema = exports_external.object({ key: KeyResourceSchema, token: exports_external.string().nullable() });
35711
+ var KeyListResponseSchema = exports_external.object({ items: exports_external.array(KeyResourceSchema), next_cursor: exports_external.uuid().nullable() });
35643
35712
  // ../contracts/src/common/canonical.ts
35644
35713
  import { createHash as createHash2 } from "crypto";
35645
35714
  function canonicalJson(value) {
@@ -35695,6 +35764,8 @@ var ERROR_CODES = {
35695
35764
  "auth.invalid_key": 401,
35696
35765
  "auth.missing_scope": 403,
35697
35766
  "auth.disabled_principal": 403,
35767
+ "principal.not_found": 404,
35768
+ "key.not_found": 404,
35698
35769
  "validation.invalid": 400,
35699
35770
  "idempotency.conflict": 409,
35700
35771
  "capacity.queue_full": 429,
@@ -35782,33 +35853,6 @@ function CursorQuerySchema(maxLimit, defaultLimit) {
35782
35853
  limit: exports_external.coerce.number().int().positive().max(maxLimit).default(defaultLimit)
35783
35854
  });
35784
35855
  }
35785
- // ../contracts/src/common/scopes.ts
35786
- var SCOPES = [
35787
- "templates:read",
35788
- "workspaces:create",
35789
- "workspaces:read",
35790
- "workspaces:cancel",
35791
- "workspaces:preserve",
35792
- "workspaces:restore",
35793
- "checkpoints:read",
35794
- "checkpoints:delete",
35795
- "outputs:read",
35796
- "conversations:read",
35797
- "conversations:delete",
35798
- "services:relay",
35799
- "attachments:write",
35800
- "logs:read",
35801
- "network:read",
35802
- "terminal:attach",
35803
- "terminal:read",
35804
- "admin"
35805
- ];
35806
- function isScope(value) {
35807
- return SCOPES.includes(value);
35808
- }
35809
- function hasScope(granted, required2) {
35810
- return granted.includes(required2) || granted.includes("admin");
35811
- }
35812
35856
  // ../contracts/src/conversations/conversation.ts
35813
35857
  var CONVERSATION_ROLES = ["user", "assistant", "system", "tool"];
35814
35858
  var ConversationMetadataSchema = exports_external.record(exports_external.string().min(1).max(64), exports_external.string().max(512)).refine((value) => Object.keys(value).length <= 32, "conversation metadata has at most 32 keys");
@@ -35935,7 +35979,7 @@ var CHECKPOINT_STATES = ["creating", "ready", "failed", "deleting", "deleted"];
35935
35979
  function isCheckpointState(value) {
35936
35980
  return CHECKPOINT_STATES.some((state) => state === value);
35937
35981
  }
35938
- var OPERATION_KINDS = ["preserve", "restore", "verify", "delete"];
35982
+ var OPERATION_KINDS = ["preserve", "restore", "verify", "delete", "purge"];
35939
35983
  var OPERATION_STATES = ["pending", "running", "succeeded", "failed"];
35940
35984
  var STORAGE_STATES = [
35941
35985
  "allocating",
@@ -37493,6 +37537,7 @@ function createWorkspaceTables(table2 = pgTable, { principals, machineKeys, temp
37493
37537
  createdAt: timestamptz("created_at").notNull(),
37494
37538
  updatedAt: timestamptz("updated_at").notNull(),
37495
37539
  terminalAt: timestamptz("terminal_at"),
37540
+ purgeRequestedAt: timestamptz("purge_requested_at"),
37496
37541
  originWorkspaceId: uuid("origin_workspace_id"),
37497
37542
  restoredFromCheckpointId: uuid("restored_from_checkpoint_id"),
37498
37543
  sourceDescriptor: structuredJson("source_descriptor").$type(),
@@ -37612,9 +37657,9 @@ var {
37612
37657
  } = createSchema();
37613
37658
  // ../db/src/database/context.ts
37614
37659
  var {SQL: SQL6 } = globalThis.Bun;
37615
- function createDatabaseContext(databaseUrl, schemaName) {
37660
+ function createDatabaseContext(databaseUrl, schemaName, options = {}) {
37616
37661
  const schema = assertValidSchema(schemaName);
37617
- const client = new SQL6(databaseUrl);
37662
+ const client = new SQL6(databaseUrl, options);
37618
37663
  return {
37619
37664
  schema,
37620
37665
  client,
@@ -37726,9 +37771,63 @@ function requiredRow(row) {
37726
37771
  return row;
37727
37772
  }
37728
37773
 
37774
+ // ../db/src/modules/auth/key-inventory.ts
37775
+ function createKeyInventory({ db, tables: { principals: principals2, machineKeys: machineKeys2 } }) {
37776
+ async function issue2(input) {
37777
+ return db.transaction(async (tx) => {
37778
+ const [principal] = await tx.select().from(principals2).where(eq(principals2.id, input.principalId)).for("update");
37779
+ if (!principal)
37780
+ throw new ApiError("auth.invalid_key", "Unknown principal.");
37781
+ if (input.issuanceRequestId) {
37782
+ const [existing] = await tx.select().from(machineKeys2).where(and(eq(machineKeys2.principalId, input.principalId), eq(machineKeys2.issuanceRequestId, input.issuanceRequestId)));
37783
+ if (existing)
37784
+ return {
37785
+ key: existing,
37786
+ created: false,
37787
+ conflict: existing.issuanceRequestDigest !== input.issuanceRequestDigest
37788
+ };
37789
+ }
37790
+ if (principal.disabledAt)
37791
+ throw new ApiError("auth.disabled_principal", "This principal is disabled.");
37792
+ if (input.issuanceRequestId && input.expiresAt && input.expiresAt <= new Date)
37793
+ throw new ApiError("validation.invalid", "Key expiry must be in the future.");
37794
+ if (input.scopes.some((scope) => !principal.scopes.includes("admin") && !principal.scopes.includes(scope))) {
37795
+ throw new ApiError("auth.missing_scope", "Key scopes exceed the principal's authority.");
37796
+ }
37797
+ const [key] = await tx.insert(machineKeys2).values(input).returning();
37798
+ return { key: requiredRow(key), created: true, conflict: false };
37799
+ });
37800
+ }
37801
+ return {
37802
+ issueMachineKey: (input) => issue2(input),
37803
+ async insertMachineKey(input) {
37804
+ await issue2(input);
37805
+ },
37806
+ async listMachineKeys(principalId, filter) {
37807
+ return db.select().from(machineKeys2).where(and(eq(machineKeys2.principalId, principalId), filter.cursor ? gt(machineKeys2.id, filter.cursor) : undefined, filter.requestId ? eq(machineKeys2.issuanceRequestId, filter.requestId) : undefined)).orderBy(asc2(machineKeys2.id)).limit(filter.limit);
37808
+ },
37809
+ async revokePrincipalKeys(principalId, at) {
37810
+ await db.transaction(async (tx) => {
37811
+ await tx.select({ id: principals2.id }).from(principals2).where(eq(principals2.id, principalId)).for("update");
37812
+ await tx.update(principals2).set({ disabledAt: at }).where(eq(principals2.id, principalId));
37813
+ await tx.update(machineKeys2).set({ revokedAt: at }).where(and(eq(machineKeys2.principalId, principalId), isNull(machineKeys2.revokedAt)));
37814
+ });
37815
+ }
37816
+ };
37817
+ }
37818
+
37729
37819
  // ../db/src/modules/auth/repository.ts
37730
- function createAuth({ db, tables: { principals: principals2, machineKeys: machineKeys2 } }) {
37820
+ function createAuth(context) {
37821
+ const {
37822
+ db,
37823
+ tables: { principals: principals2, machineKeys: machineKeys2 }
37824
+ } = context;
37731
37825
  return {
37826
+ ...createKeyInventory(context),
37827
+ async getPrincipal(id) {
37828
+ const [row] = await db.select().from(principals2).where(eq(principals2.id, id));
37829
+ return row ?? null;
37830
+ },
37732
37831
  async createPrincipal(name2, scopes, templateNames) {
37733
37832
  const [row] = await db.insert(principals2).values({ id: randomUUID2(), name: name2, scopes, templateNames, createdAt: sql`now()` }).returning();
37734
37833
  return requiredRow(row);
@@ -37747,9 +37846,6 @@ function createAuth({ db, tables: { principals: principals2, machineKeys: machin
37747
37846
  async setPrincipalDisabled(id, disabled) {
37748
37847
  await db.update(principals2).set({ disabledAt: disabled ? sql`now()` : null }).where(eq(principals2.id, id));
37749
37848
  },
37750
- async insertMachineKey(row) {
37751
- await db.insert(machineKeys2).values(row);
37752
- },
37753
37849
  async getMachineKeyWithPrincipal(keyId) {
37754
37850
  const [row] = await db.select({ key: machineKeys2, principal: principals2 }).from(machineKeys2).innerJoin(principals2, eq(principals2.id, machineKeys2.principalId)).where(eq(machineKeys2.id, keyId));
37755
37851
  return row ?? null;
@@ -37764,16 +37860,71 @@ function createAuth({ db, tables: { principals: principals2, machineKeys: machin
37764
37860
  };
37765
37861
  }
37766
37862
 
37863
+ // ../db/src/modules/persistence/content.ts
37864
+ async function contentWritable(tx, tables, workspaceId) {
37865
+ const [workspace] = await tx.select({ purgeRequestedAt: tables.workspaces.purgeRequestedAt }).from(tables.workspaces).where(eq(tables.workspaces.id, workspaceId)).for("update");
37866
+ return !workspace?.purgeRequestedAt;
37867
+ }
37868
+ async function requireContentWritable(tx, tables, workspaceId) {
37869
+ if (!await contentWritable(tx, tables, workspaceId)) {
37870
+ throw new ApiError("operation.conflict", "Workspace content is being purged.");
37871
+ }
37872
+ }
37873
+ function createContentPurge({ db, tables }) {
37874
+ return {
37875
+ async listWorkspaceStorage(workspaceId) {
37876
+ return db.select().from(tables.workspaceStorage).where(eq(tables.workspaceStorage.workspaceId, workspaceId));
37877
+ },
37878
+ async purgeWorkspaceContent(workspaceId, at) {
37879
+ await db.transaction(async (tx) => {
37880
+ const [workspace] = await tx.select().from(tables.workspaces).where(eq(tables.workspaces.id, workspaceId)).for("update");
37881
+ if (!workspace?.purgeRequestedAt)
37882
+ throw new Error("Purge has not been admitted");
37883
+ for (const table2 of [
37884
+ tables.workspaceLogs,
37885
+ tables.workspaceOutputs,
37886
+ tables.workspaceConversationMessages,
37887
+ tables.workspaceNetworkEvents,
37888
+ tables.eventOutbox
37889
+ ]) {
37890
+ await tx.delete(table2).where(eq(table2.workspaceId, workspaceId));
37891
+ }
37892
+ await tx.insert(tables.workspaceConversations).values({ workspaceId, status: "deleted", deletedAt: at, updatedAt: at }).onConflictDoUpdate({
37893
+ target: tables.workspaceConversations.workspaceId,
37894
+ set: { status: "deleted", expiresAt: null, deletedAt: at, updatedAt: at }
37895
+ });
37896
+ await tx.update(tables.workspaceCheckpoints).set({ manifest: null, manifestDigest: null, sourceProvenance: null, label: null }).where(eq(tables.workspaceCheckpoints.workspaceId, workspaceId));
37897
+ await tx.update(tables.workspaces).set({
37898
+ launchInput: null,
37899
+ outputs: {},
37900
+ metadata: {},
37901
+ health: {},
37902
+ failureLogTail: null,
37903
+ failureLogTailTruncated: false,
37904
+ failureLastLogSeq: null,
37905
+ sourceDescriptor: null,
37906
+ resolvedSource: null,
37907
+ registrationDigest: null,
37908
+ reconnectDigest: null,
37909
+ updatedAt: at
37910
+ }).where(eq(tables.workspaces.id, workspaceId));
37911
+ });
37912
+ }
37913
+ };
37914
+ }
37915
+
37767
37916
  // ../db/src/modules/conversations/repository.ts
37768
37917
  var MAX_CONVERSATION_BYTES = 50 * 1024 * 1024;
37769
37918
  var MAX_CONVERSATION_MESSAGES = 1e5;
37770
- function createConversations({
37771
- db,
37772
- tables: { workspaceConversations: conversations2, workspaceConversationMessages: messages }
37773
- }) {
37919
+ function createConversations(context) {
37920
+ const {
37921
+ db,
37922
+ tables: { workspaceConversations: conversations2, workspaceConversationMessages: messages }
37923
+ } = context;
37774
37924
  return {
37775
37925
  async appendConversationMessage(input) {
37776
37926
  return db.transaction(async (tx) => {
37927
+ await requireContentWritable(tx, context.tables, input.workspaceId);
37777
37928
  await lock(tx, input.workspaceId, 7081);
37778
37929
  const [state] = await tx.select({ status: conversations2.status }).from(conversations2).where(eq(conversations2.workspaceId, input.workspaceId)).for("update");
37779
37930
  if (state?.status === "deleted")
@@ -37827,12 +37978,18 @@ function createConversations({
37827
37978
 
37828
37979
  // ../db/src/modules/logs/repository.ts
37829
37980
  var MAX_LOG_BYTES = 10 * 1024 * 1024;
37830
- function createLogs({ db, tables: { workspaceLogs: logs2 } }) {
37981
+ function createLogs(context) {
37982
+ const {
37983
+ db,
37984
+ tables: { workspaceLogs: logs2 }
37985
+ } = context;
37831
37986
  return {
37832
37987
  async appendLogs(workspaceId, entries) {
37833
37988
  if (entries.length === 0)
37834
37989
  return;
37835
37990
  await db.transaction(async (tx) => {
37991
+ if (!await contentWritable(tx, context.tables, workspaceId))
37992
+ return;
37836
37993
  await lock(tx, workspaceId, 7080);
37837
37994
  const [stats] = await tx.select({
37838
37995
  maxSeq: sql`coalesce(max(${logs2.seq}),0)`.mapWith(Number),
@@ -37872,12 +38029,18 @@ function createLogs({ db, tables: { workspaceLogs: logs2 } }) {
37872
38029
  }
37873
38030
 
37874
38031
  // ../db/src/modules/network-audit/repository.ts
37875
- function createNetworkAudit({ db, tables: { workspaceNetworkEvents: events, workspaces: workspaces3 } }) {
38032
+ function createNetworkAudit(context) {
38033
+ const {
38034
+ db,
38035
+ tables: { workspaceNetworkEvents: events, workspaces: workspaces3 }
38036
+ } = context;
37876
38037
  return {
37877
38038
  async appendNetworkEvents(workspaceId, sourceSessionId, inputs) {
37878
38039
  if (inputs.length === 0)
37879
38040
  return;
37880
38041
  await db.transaction(async (tx) => {
38042
+ if (!await contentWritable(tx, context.tables, workspaceId))
38043
+ return;
37881
38044
  await lock(tx, workspaceId, 7348);
37882
38045
  for (const event of inputs) {
37883
38046
  const [duplicate] = await tx.select({ seq: events.seq }).from(events).where(and(eq(events.workspaceId, workspaceId), eq(events.sourceSessionId, sourceSessionId), eq(events.sourceSeq, event.source_seq)));
@@ -37919,7 +38082,11 @@ function createNetworkAudit({ db, tables: { workspaceNetworkEvents: events, work
37919
38082
  // ../db/src/modules/outbox/repository.ts
37920
38083
  import { randomUUID as randomUUID3 } from "crypto";
37921
38084
  var CLAIM_LEASE_MS = 60000;
37922
- function createOutbox({ db, tables: { eventOutbox: outbox2 } }) {
38085
+ function createOutbox(context) {
38086
+ const {
38087
+ db,
38088
+ tables: { eventOutbox: outbox2 }
38089
+ } = context;
37923
38090
  return {
37924
38091
  async claimDueEvents(now, limit) {
37925
38092
  const due = db.select({ id: outbox2.id }).from(outbox2).where(and(isNull(outbox2.deliveredAt), lte(outbox2.nextAttemptAt, now))).orderBy(asc2(outbox2.occurredAt)).limit(limit).for("update", { skipLocked: true });
@@ -37933,23 +38100,32 @@ function createOutbox({ db, tables: { eventOutbox: outbox2 } }) {
37933
38100
  await db.update(outbox2).set({ attemptCount: sql`${outbox2.attemptCount}+1`, lastErrorCode: errorCode, nextAttemptAt }).where(eq(outbox2.id, id));
37934
38101
  },
37935
38102
  async appendEvent(workspaceId, eventType, payload, at) {
37936
- await db.insert(outbox2).values({
37937
- id: randomUUID3(),
37938
- workspaceId,
37939
- eventType,
37940
- payload: payload === null ? sql`'null'::jsonb` : payload,
37941
- occurredAt: at,
37942
- nextAttemptAt: at
38103
+ await db.transaction(async (tx) => {
38104
+ if (!await contentWritable(tx, context.tables, workspaceId))
38105
+ return;
38106
+ await tx.insert(outbox2).values({
38107
+ id: randomUUID3(),
38108
+ workspaceId,
38109
+ eventType,
38110
+ payload: payload === null ? sql`'null'::jsonb` : payload,
38111
+ occurredAt: at,
38112
+ nextAttemptAt: at
38113
+ });
37943
38114
  });
37944
38115
  }
37945
38116
  };
37946
38117
  }
37947
38118
 
37948
38119
  // ../db/src/modules/outputs/repository.ts
37949
- function createOutputs({ db, tables: { workspaceOutputs: outputs2, workspaces: workspaces3 } }) {
38120
+ function createOutputs(context) {
38121
+ const {
38122
+ db,
38123
+ tables: { workspaceOutputs: outputs2, workspaces: workspaces3 }
38124
+ } = context;
37950
38125
  return {
37951
38126
  async appendOutput(input) {
37952
38127
  return db.transaction(async (tx) => {
38128
+ await requireContentWritable(tx, context.tables, input.workspaceId);
37953
38129
  await lock(tx, input.workspaceId, 7081);
37954
38130
  const [latest] = await tx.select({ seq: sql`coalesce(max(${outputs2.seq}),0)`.mapWith(Number) }).from(outputs2).where(eq(outputs2.workspaceId, input.workspaceId));
37955
38131
  const row = { ...input, seq: requiredRow(latest).seq + 1 };
@@ -37997,7 +38173,7 @@ function createCheckpoints({ db, tables: { workspaceCheckpoints: checkpoints } }
37997
38173
  function createOperations({
37998
38174
  db,
37999
38175
  schema,
38000
- tables: { workspaceOperations: operations, workspaceCheckpoints: checkpoints }
38176
+ tables: { workspaceOperations: operations, workspaceCheckpoints: checkpoints, workspaces: workspaces3 }
38001
38177
  }) {
38002
38178
  const incomplete = inArray(operations.state, ["pending", "running"]);
38003
38179
  async function countIncomplete(tx = db) {
@@ -38011,6 +38187,15 @@ function createOperations({
38011
38187
  const [existing] = await tx.select().from(operations).where(and(eq(operations.principalId, input.principalId), eq(operations.kind, input.kind), eq(operations.idempotencyKey, input.idempotencyKey)));
38012
38188
  if (existing)
38013
38189
  return { operation: existing, created: false, conflict: existing.requestDigest !== input.requestDigest };
38190
+ if (input.workspaceId) {
38191
+ const [workspace] = await tx.select().from(workspaces3).where(eq(workspaces3.id, input.workspaceId)).for("update");
38192
+ if (workspace?.purgeRequestedAt && input.kind !== "purge") {
38193
+ throw new ApiError("operation.conflict", "Workspace content is being purged.");
38194
+ }
38195
+ if (input.kind === "purge") {
38196
+ await tx.update(workspaces3).set({ purgeRequestedAt: workspace?.purgeRequestedAt ?? input.createdAt }).where(eq(workspaces3.id, input.workspaceId));
38197
+ }
38198
+ }
38014
38199
  if (options.maxIncompleteOperations !== undefined && await countIncomplete(tx) >= options.maxIncompleteOperations)
38015
38200
  throw new OperationCapacityExceededError;
38016
38201
  const [row] = await tx.insert(operations).values(input).returning();
@@ -38129,6 +38314,152 @@ function createTerminals({ db, tables: { workspaceTerminalSessions: sessions } }
38129
38314
 
38130
38315
  // ../db/src/modules/workspaces/events.ts
38131
38316
  import { randomUUID as randomUUID8 } from "crypto";
38317
+ // ../auth/src/index.ts
38318
+ import { createHmac, randomBytes, randomUUID as randomUUID5, timingSafeEqual } from "crypto";
38319
+ var KEY_PREFIX = "pkt";
38320
+ function issueMachineKey(pepper) {
38321
+ const id = randomUUID5();
38322
+ const secret = randomBytes(32).toString("base64url");
38323
+ return {
38324
+ id,
38325
+ token: `${KEY_PREFIX}_${id}_${secret}`,
38326
+ secretDigest: digestSecret(pepper, id, secret)
38327
+ };
38328
+ }
38329
+ var KEY_RE = /^pkt_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})_([A-Za-z0-9_-]+)$/;
38330
+ function parseMachineKey(token) {
38331
+ const match = KEY_RE.exec(token);
38332
+ if (!match) {
38333
+ return null;
38334
+ }
38335
+ return { id: match[1], secret: match[2] };
38336
+ }
38337
+ function digestSecret(pepper, keyId, secret) {
38338
+ return new Uint8Array(createHmac("sha256", pepper).update(`${keyId}:${secret}`).digest());
38339
+ }
38340
+ function verifySecret(pepper, keyId, secret, storedDigest) {
38341
+ const computed = digestSecret(pepper, keyId, secret);
38342
+ if (computed.length !== storedDigest.length) {
38343
+ return false;
38344
+ }
38345
+ return timingSafeEqual(computed, storedDigest);
38346
+ }
38347
+ function generateOpaqueSecret() {
38348
+ return randomBytes(32).toString("base64url");
38349
+ }
38350
+ function digestOpaque(pepper, secret) {
38351
+ return new Uint8Array(createHmac("sha256", pepper).update(secret).digest());
38352
+ }
38353
+ function verifyOpaque(pepper, secret, storedDigest) {
38354
+ const computed = digestOpaque(pepper, secret);
38355
+ if (computed.length !== storedDigest.length) {
38356
+ return false;
38357
+ }
38358
+ return timingSafeEqual(computed, storedDigest);
38359
+ }
38360
+ function signEvent(signingKey, timestamp4, body) {
38361
+ const mac3 = createHmac("sha256", signingKey).update(`${timestamp4}.${body}`).digest("hex");
38362
+ return `sha256=${mac3}`;
38363
+ }
38364
+ var EGRESS_TOKEN_PREFIX = "pce1";
38365
+ var EGRESS_TOKEN_DOMAIN = "pocketcoder-egress-audit-v1\x00";
38366
+ function issueEgressAuditToken(signingKey, subject) {
38367
+ const payload = Buffer.from(JSON.stringify({
38368
+ v: 1,
38369
+ aud: "egress-audit",
38370
+ kind: subject.kind,
38371
+ id: subject.id,
38372
+ exp: subject.expiresAt.getTime()
38373
+ })).toString("base64url");
38374
+ const signature = createHmac("sha256", signingKey).update(`${EGRESS_TOKEN_DOMAIN}${payload}`).digest("base64url");
38375
+ return `${EGRESS_TOKEN_PREFIX}.${payload}.${signature}`;
38376
+ }
38377
+ function verifyEgressAuditToken(signingKey, token, now = new Date) {
38378
+ const [prefix, payload, signature, extra] = token.split(".");
38379
+ if (prefix !== EGRESS_TOKEN_PREFIX || !payload || !signature || extra)
38380
+ return null;
38381
+ const expected = createHmac("sha256", signingKey).update(`${EGRESS_TOKEN_DOMAIN}${payload}`).digest("base64url");
38382
+ const actualBytes = Buffer.from(signature);
38383
+ const expectedBytes = Buffer.from(expected);
38384
+ if (actualBytes.length !== expectedBytes.length || !timingSafeEqual(actualBytes, expectedBytes)) {
38385
+ return null;
38386
+ }
38387
+ try {
38388
+ const value = JSON.parse(Buffer.from(payload, "base64url").toString());
38389
+ if (value.v !== 1 || value.aud !== "egress-audit" || value.kind !== "workspace" && value.kind !== "pool" || typeof value.id !== "string" || !value.id || typeof value.exp !== "number" || value.exp <= now.getTime()) {
38390
+ return null;
38391
+ }
38392
+ return { kind: value.kind, id: value.id };
38393
+ } catch {
38394
+ return null;
38395
+ }
38396
+ }
38397
+ var REDACT_PATTERNS = [/pkt_[0-9a-f-]{36}_[A-Za-z0-9_-]+/g, /(authorization:?\s*bearer\s+)\S+/gi];
38398
+ function redact(text6) {
38399
+ let out = text6;
38400
+ for (const pattern of REDACT_PATTERNS) {
38401
+ out = out.replace(pattern, "[redacted]");
38402
+ }
38403
+ return out;
38404
+ }
38405
+
38406
+ // ../runtime-core/src/keys/key-administration.ts
38407
+ function keyResource(key, principal) {
38408
+ const scopes = key.scopes.length ? key.scopes : principal.scopes;
38409
+ return {
38410
+ id: key.id,
38411
+ principal_id: key.principalId,
38412
+ scopes: key.scopes,
38413
+ effective_scopes: scopes.filter((scope) => principal.scopes.includes("admin") || principal.scopes.includes(scope)),
38414
+ managed_principal_ids: key.managedPrincipalIds,
38415
+ issuance_request_id: key.issuanceRequestId,
38416
+ created_at: key.createdAt.toISOString(),
38417
+ expires_at: key.expiresAt?.toISOString() ?? null,
38418
+ revoked_at: key.revokedAt?.toISOString() ?? null,
38419
+ last_used_at: key.lastUsedAt?.toISOString() ?? null
38420
+ };
38421
+ }
38422
+ async function issuePrincipalKey(store, pepper, principal, request, options = {}) {
38423
+ const scopes = [...new Set(request.scopes)].sort();
38424
+ const managedPrincipalIds = [...new Set(options.managedPrincipalIds ?? [])].sort();
38425
+ if (scopes.some((scope) => !(principal.scopes.includes("admin") || principal.scopes.includes(scope)))) {
38426
+ throw new ApiError("auth.missing_scope", "Key scopes exceed the principal's authority.");
38427
+ }
38428
+ if (!options.operatorBootstrap && scopes.some((scope) => ["admin", "keys:read", "keys:write", "workspaces:recover"].includes(scope))) {
38429
+ throw new ApiError("auth.missing_scope", "Delegated issuance cannot grant administrative authority.");
38430
+ }
38431
+ const expiresAt = request.expires_at ? new Date(request.expires_at) : null;
38432
+ if (!expiresAt && !options.operatorBootstrap || expiresAt && !Number.isFinite(expiresAt.getTime()))
38433
+ throw new ApiError("validation.invalid", "Invalid key expiry.");
38434
+ if (managedPrincipalIds.length && (!options.operatorBootstrap || !principal.scopes.includes("admin"))) {
38435
+ throw new ApiError("auth.missing_scope", "Only an operator can bootstrap delegated authority.");
38436
+ }
38437
+ const generated = issueMachineKey(pepper);
38438
+ const result = await store.issueMachineKey({
38439
+ id: generated.id,
38440
+ secretDigest: generated.secretDigest,
38441
+ principalId: principal.id,
38442
+ scopes,
38443
+ managedPrincipalIds,
38444
+ issuanceRequestId: request.request_id,
38445
+ issuanceRequestDigest: digestOf({
38446
+ scopes,
38447
+ expires_at: expiresAt?.toISOString() ?? null,
38448
+ managed_principal_ids: managedPrincipalIds
38449
+ }),
38450
+ createdAt: new Date,
38451
+ expiresAt,
38452
+ revokedAt: null,
38453
+ lastUsedAt: null
38454
+ });
38455
+ if (result.conflict)
38456
+ throw new ApiError("idempotency.conflict", "Changed key issuance request.");
38457
+ return {
38458
+ created: result.created,
38459
+ key: keyResource(result.key, principal),
38460
+ token: result.created ? generated.token : null
38461
+ };
38462
+ }
38132
38463
  // ../runtime-core/src/observability/metrics.ts
38133
38464
  function metricKey(name2, labels = {}) {
38134
38465
  const entries = Object.entries(labels).sort(([left2], [right2]) => left2.localeCompare(right2));
@@ -38224,6 +38555,25 @@ class OutboxDispatcher {
38224
38555
  await this.deps.store.markEventFailed(id, errorCode, new Date(this.now().getTime() + backoff));
38225
38556
  }
38226
38557
  }
38558
+ // ../runtime-core/src/scheduler/provider-termination.ts
38559
+ async function stopWorkspaceProvider(store, driver, workspace, graceSeconds, at, remove = true) {
38560
+ if (!workspace.providerRef)
38561
+ return;
38562
+ const ref = { kind: workspace.providerKind ?? "", id: "", ...workspace.providerRef };
38563
+ await driver.stop(ref, graceSeconds);
38564
+ const evidence = await driver.terminationEvidence?.(ref);
38565
+ if (evidence) {
38566
+ const current = await store.getWorkspace(workspace.id);
38567
+ if (!current?.providerRef || current.providerRef.id !== workspace.providerRef.id)
38568
+ throw new Error("Termination provider changed");
38569
+ await store.updateWorkspace(workspace.id, {
38570
+ providerRef: { ...current.providerRef, terminationEvidence: evidence }
38571
+ }, at);
38572
+ }
38573
+ if (remove)
38574
+ await driver.remove(ref);
38575
+ }
38576
+
38227
38577
  // ../runtime-core/src/reconciliation/reconciliation-metrics.ts
38228
38578
  async function measureReconciliation(metrics, kind, skipped, reconcile) {
38229
38579
  const startedAt = performance.now();
@@ -38278,8 +38628,7 @@ async function reconcileDeletion(deps, operation, checkpoint, now) {
38278
38628
  async function cleanupProvider(deps, workspace) {
38279
38629
  if (!workspace?.providerRef)
38280
38630
  return;
38281
- await deps.driver.stop(workspace.providerRef, 1).catch(() => {});
38282
- await deps.driver.remove(workspace.providerRef).catch(() => {});
38631
+ await stopWorkspaceProvider(deps.store, deps.driver, workspace, 1, deps.now?.() ?? new Date);
38283
38632
  }
38284
38633
  async function completePreserve(deps, operation, checkpoint, workspace, now) {
38285
38634
  await cleanupProvider(deps, workspace);
@@ -38314,13 +38663,26 @@ async function failPreserve(deps, operation, context, now) {
38314
38663
  }
38315
38664
  await failOperation(deps, operation, "checkpoint_failed", now);
38316
38665
  }
38666
+ async function reconcileRestore(deps, operation, now) {
38667
+ const target = operation.resultWorkspaceId ? await deps.store.getWorkspace(operation.resultWorkspaceId) : await deps.store.getWorkspaceByIdempotency(operation.principalId, `restore:${operation.id}`);
38668
+ if (!target) {
38669
+ await failOperation(deps, operation, "restore_failed", now);
38670
+ return;
38671
+ }
38672
+ if (!operation.resultWorkspaceId) {
38673
+ await deps.store.updateOperation(operation.id, { resultWorkspaceId: target.id }, now);
38674
+ }
38675
+ if (isTerminal(target.state))
38676
+ await failOperation(deps, operation, "restore_failed", now);
38677
+ }
38317
38678
  async function reconcileOperation(deps, operation, now) {
38318
- const context = await loadOperationContext(deps, operation);
38679
+ if (operation.kind === "purge")
38680
+ return;
38319
38681
  if (operation.kind === "restore") {
38320
- if (!context.workspace)
38321
- await failOperation(deps, operation, "restore_failed", now);
38682
+ await reconcileRestore(deps, operation, now);
38322
38683
  return;
38323
38684
  }
38685
+ const context = await loadOperationContext(deps, operation);
38324
38686
  if (!context.checkpoint) {
38325
38687
  await failOperation(deps, operation, "checkpoint_storage_lost", now);
38326
38688
  return;
@@ -38372,22 +38734,13 @@ async function reconcilePersistence(deps) {
38372
38734
  }
38373
38735
  // ../runtime-core/src/reconciliation/reconcile.ts
38374
38736
  async function reconcileProviderRow(deps, row, found, now) {
38375
- if (row.state === "queued")
38737
+ if (row.state === "queued" || row.state === "terminating")
38376
38738
  return;
38377
38739
  const mismatched = found && found.templateDigest !== row.templateDigest;
38378
38740
  if (mismatched) {
38379
38741
  deps.log?.(`reconcile: template digest mismatch for ${row.id}; failing workspace`);
38380
38742
  }
38381
38743
  const lost = !found || mismatched;
38382
- if (lost && row.state === "terminating") {
38383
- await deps.store.transition(row.id, {
38384
- from: ["terminating"],
38385
- to: row.terminalIntent ?? "failed",
38386
- reason: row.reasonCode ?? "provider_lost",
38387
- at: now
38388
- });
38389
- return;
38390
- }
38391
38744
  if (lost && (row.state === "connected" || row.state === "ready")) {
38392
38745
  await settleLostStorage(deps, row, now);
38393
38746
  await deps.store.transition(row.id, {
@@ -38510,95 +38863,6 @@ async function loadTemplateDir(store, dir) {
38510
38863
  }
38511
38864
  return result;
38512
38865
  }
38513
- // ../auth/src/index.ts
38514
- import { createHmac, randomBytes, randomUUID as randomUUID5, timingSafeEqual } from "crypto";
38515
- var KEY_PREFIX = "pkt";
38516
- function issueMachineKey(pepper) {
38517
- const id = randomUUID5();
38518
- const secret = randomBytes(32).toString("base64url");
38519
- return {
38520
- id,
38521
- token: `${KEY_PREFIX}_${id}_${secret}`,
38522
- secretDigest: digestSecret(pepper, id, secret)
38523
- };
38524
- }
38525
- var KEY_RE = /^pkt_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})_([A-Za-z0-9_-]+)$/;
38526
- function parseMachineKey(token) {
38527
- const match = KEY_RE.exec(token);
38528
- if (!match) {
38529
- return null;
38530
- }
38531
- return { id: match[1], secret: match[2] };
38532
- }
38533
- function digestSecret(pepper, keyId, secret) {
38534
- return new Uint8Array(createHmac("sha256", pepper).update(`${keyId}:${secret}`).digest());
38535
- }
38536
- function verifySecret(pepper, keyId, secret, storedDigest) {
38537
- const computed = digestSecret(pepper, keyId, secret);
38538
- if (computed.length !== storedDigest.length) {
38539
- return false;
38540
- }
38541
- return timingSafeEqual(computed, storedDigest);
38542
- }
38543
- function generateOpaqueSecret() {
38544
- return randomBytes(32).toString("base64url");
38545
- }
38546
- function digestOpaque(pepper, secret) {
38547
- return new Uint8Array(createHmac("sha256", pepper).update(secret).digest());
38548
- }
38549
- function verifyOpaque(pepper, secret, storedDigest) {
38550
- const computed = digestOpaque(pepper, secret);
38551
- if (computed.length !== storedDigest.length) {
38552
- return false;
38553
- }
38554
- return timingSafeEqual(computed, storedDigest);
38555
- }
38556
- function signEvent(signingKey, timestamp4, body) {
38557
- const mac3 = createHmac("sha256", signingKey).update(`${timestamp4}.${body}`).digest("hex");
38558
- return `sha256=${mac3}`;
38559
- }
38560
- var EGRESS_TOKEN_PREFIX = "pce1";
38561
- var EGRESS_TOKEN_DOMAIN = "pocketcoder-egress-audit-v1\x00";
38562
- function issueEgressAuditToken(signingKey, subject) {
38563
- const payload = Buffer.from(JSON.stringify({
38564
- v: 1,
38565
- aud: "egress-audit",
38566
- kind: subject.kind,
38567
- id: subject.id,
38568
- exp: subject.expiresAt.getTime()
38569
- })).toString("base64url");
38570
- const signature = createHmac("sha256", signingKey).update(`${EGRESS_TOKEN_DOMAIN}${payload}`).digest("base64url");
38571
- return `${EGRESS_TOKEN_PREFIX}.${payload}.${signature}`;
38572
- }
38573
- function verifyEgressAuditToken(signingKey, token, now = new Date) {
38574
- const [prefix, payload, signature, extra] = token.split(".");
38575
- if (prefix !== EGRESS_TOKEN_PREFIX || !payload || !signature || extra)
38576
- return null;
38577
- const expected = createHmac("sha256", signingKey).update(`${EGRESS_TOKEN_DOMAIN}${payload}`).digest("base64url");
38578
- const actualBytes = Buffer.from(signature);
38579
- const expectedBytes = Buffer.from(expected);
38580
- if (actualBytes.length !== expectedBytes.length || !timingSafeEqual(actualBytes, expectedBytes)) {
38581
- return null;
38582
- }
38583
- try {
38584
- const value = JSON.parse(Buffer.from(payload, "base64url").toString());
38585
- if (value.v !== 1 || value.aud !== "egress-audit" || value.kind !== "workspace" && value.kind !== "pool" || typeof value.id !== "string" || !value.id || typeof value.exp !== "number" || value.exp <= now.getTime()) {
38586
- return null;
38587
- }
38588
- return { kind: value.kind, id: value.id };
38589
- } catch {
38590
- return null;
38591
- }
38592
- }
38593
- var REDACT_PATTERNS = [/pkt_[0-9a-f-]{36}_[A-Za-z0-9_-]+/g, /(authorization:?\s*bearer\s+)\S+/gi];
38594
- function redact(text6) {
38595
- let out = text6;
38596
- for (const pattern of REDACT_PATTERNS) {
38597
- out = out.replace(pattern, "[redacted]");
38598
- }
38599
- return out;
38600
- }
38601
-
38602
38866
  // ../runtime-core/src/scheduler/scheduler-base.ts
38603
38867
  var FAILURE_LOG_TAIL_BYTES = 16 * 1024;
38604
38868
  function decodeFailureLogTail(content, truncated) {
@@ -38785,8 +39049,20 @@ class SchedulerAdmission {
38785
39049
  return;
38786
39050
  }
38787
39051
  }
39052
+ async authorized(row) {
39053
+ if (!this.context.deps.authorizeLaunch)
39054
+ return true;
39055
+ try {
39056
+ return await this.context.deps.authorizeLaunch(row) === true;
39057
+ } catch {
39058
+ this.context.report(`admit.policy.${row.id}`, new Error("Launch policy unavailable"));
39059
+ return false;
39060
+ }
39061
+ }
38788
39062
  async launch(row) {
38789
39063
  const { store, driver, secrets } = this.context.deps;
39064
+ if (!await this.authorized(row))
39065
+ return false;
38790
39066
  const now = this.context.now();
38791
39067
  const secret = secrets.generate();
38792
39068
  const registrationDigest = secrets.digest(secret);
@@ -38951,24 +39227,28 @@ class SchedulerLifecycle {
38951
39227
  connections.signal(row.id, "TERM");
38952
39228
  }
38953
39229
  if (updated.providerRef) {
38954
- this.stopAndRemove({ kind: updated.providerKind ?? "", id: "", ...updated.providerRef }, this.context.graceSeconds(updated)).then(() => this.finalize(updated, terminalState, reason, this.context.now())).catch((err) => this.context.report(`terminate.${row.id}`, err));
39230
+ this.finalize(updated, terminalState, reason, this.context.now()).catch((err) => this.context.report(`terminate.${row.id}`, err));
38955
39231
  } else {
38956
39232
  await this.finalize(updated, terminalState, reason, at);
38957
39233
  }
38958
39234
  return updated;
38959
39235
  }
38960
- async finalize(row, terminalState, reason, at, retainStorage = false) {
39236
+ async finalize(row, terminalState, reason, at) {
38961
39237
  const { store, driver, connections } = this.context.deps;
39238
+ const retainStorage = terminalState === "failed" && row.templateSnapshot.spec.persistence.checkpoint.onFailure === "retain-for-recovery";
38962
39239
  if (row.providerRef) {
38963
39240
  try {
38964
- await driver.stop({ kind: row.providerKind ?? "", id: "", ...row.providerRef }, this.context.graceSeconds(row));
38965
- await driver.remove({
38966
- kind: row.providerKind ?? "",
38967
- id: "",
38968
- ...row.providerRef
38969
- });
39241
+ await stopWorkspaceProvider(store, driver, row, this.context.graceSeconds(row), at);
38970
39242
  } catch (err) {
38971
39243
  this.context.report(`finalize.terminate.${row.id}`, err);
39244
+ await store.transition(row.id, {
39245
+ from: ["provisioning", "connected", "ready"],
39246
+ to: "terminating",
39247
+ reason,
39248
+ at,
39249
+ patch: { terminalIntent: terminalState, registrationDigest: null }
39250
+ });
39251
+ return;
38972
39252
  }
38973
39253
  }
38974
39254
  if (retainStorage) {
@@ -39008,16 +39288,12 @@ class SchedulerLifecycle {
39008
39288
  }
39009
39289
  });
39010
39290
  }
39011
- async stopAndRemove(ref, graceSeconds) {
39012
- await this.context.deps.driver.stop(ref, graceSeconds);
39013
- await this.context.deps.driver.remove(ref);
39014
- }
39015
39291
  async fail(row, reason, at) {
39016
39292
  const action = row.templateSnapshot.spec.persistence.checkpoint.onFailure;
39017
39293
  if (action === "preserve" && ["connected", "ready"].includes(row.state) && await this.requestPolicyPreserve(row, "failure")) {
39018
39294
  return;
39019
39295
  }
39020
- await this.finalize(row, "failed", reason, at, action === "retain-for-recovery");
39296
+ await this.finalize(row, "failed", reason, at);
39021
39297
  }
39022
39298
  async handleProcessExit(row, exitCode, at) {
39023
39299
  if (exitCode === 0 && row.templateSnapshot.spec.persistence.checkpoint.onCleanExit === "preserve" && await this.requestPolicyPreserve(row, "clean_exit")) {
@@ -39062,6 +39338,8 @@ class SchedulerSweep {
39062
39338
  for (const row of rows) {
39063
39339
  try {
39064
39340
  await this.sweepRow(row, now);
39341
+ if (row.providerRef)
39342
+ await this.context.deps.driver.inspect(row.providerRef);
39065
39343
  } catch (err) {
39066
39344
  this.context.report(`sweep.${row.id}`, err);
39067
39345
  }
@@ -39109,9 +39387,12 @@ class SchedulerSweep {
39109
39387
  }
39110
39388
  async sweepTerminating(row, now) {
39111
39389
  const stuckMs = 4 * this.context.timeoutMs(row, "terminateGrace") + 5000;
39112
- if (now.getTime() - row.updatedAt.getTime() <= stuckMs)
39390
+ const history = await this.context.deps.store.listStateHistory(row.id);
39391
+ const termination = history.find((entry) => entry.toState === "terminating");
39392
+ if (!termination || now.getTime() - termination.occurredAt.getTime() <= stuckMs)
39113
39393
  return;
39114
- await this.lifecycle.finalize(row, row.terminalIntent ?? "failed", row.reasonCode, now);
39394
+ const terminalState = row.terminalIntent ?? "failed";
39395
+ await this.lifecycle.finalize(row, terminalState, row.reasonCode, now);
39115
39396
  }
39116
39397
  async sweepRow(row, now) {
39117
39398
  switch (row.state) {
@@ -39122,8 +39403,8 @@ class SchedulerSweep {
39122
39403
  if (row.registrationExpiresAt && now >= row.registrationExpiresAt) {
39123
39404
  if (row.provisioningMode === "warm" && row.launchAttempts < this.context.deps.limits.maxLaunchAttempts) {
39124
39405
  if (row.providerRef) {
39125
- await this.context.deps.driver.stop(row.providerRef, 1).catch(() => {});
39126
- await this.context.deps.driver.remove(row.providerRef).catch(() => {});
39406
+ await this.context.deps.driver.stop(row.providerRef, 1);
39407
+ await this.context.deps.driver.remove(row.providerRef);
39127
39408
  }
39128
39409
  await this.context.deps.store.transition(row.id, {
39129
39410
  from: ["provisioning"],
@@ -39181,6 +39462,9 @@ class Scheduler {
39181
39462
  });
39182
39463
  return this.context.activeTick;
39183
39464
  }
39465
+ async drain() {
39466
+ await this.context.activeTick;
39467
+ }
39184
39468
  async runTick() {
39185
39469
  await this.sweeper.sweep();
39186
39470
  await this.admission.admit();
@@ -39476,6 +39760,8 @@ async function appendTransition(context, tx, row, from, reason, at) {
39476
39760
  reasonCode: reason,
39477
39761
  occurredAt: at
39478
39762
  });
39763
+ if (row.purgeRequestedAt)
39764
+ return;
39479
39765
  const payload = buildEventEnvelope(row, at);
39480
39766
  await tx.insert(context.tables.eventOutbox).values({
39481
39767
  id: payload.id,
@@ -39520,7 +39806,7 @@ function createWarmPools(context) {
39520
39806
  async claimWarmPoolRuntime(claim) {
39521
39807
  const result = await db.transaction(async (tx) => {
39522
39808
  const [current] = await tx.select().from(workspaces3).where(eq(workspaces3.id, claim.workspaceId)).for("update");
39523
- if (current?.state !== "queued")
39809
+ if (current?.state !== "queued" || current.purgeRequestedAt)
39524
39810
  return null;
39525
39811
  const [runtime] = await tx.select().from(runtimes).where(and(eq(runtimes.templateDigest, claim.templateDigest), eq(runtimes.driverKind, claim.driverKind), eq(runtimes.eligibilityFingerprint, claim.eligibilityFingerprint), eq(runtimes.state, "ready"), isNotNull(runtimes.providerRef))).orderBy(asc2(runtimes.readyAt)).limit(1).for("update", { skipLocked: true });
39526
39812
  if (!runtime)
@@ -39586,7 +39872,7 @@ function createAdmission(context) {
39586
39872
  const workspace = await db.transaction(async (tx) => {
39587
39873
  await lock(tx, `${schema}:workspace-admission`, 7351);
39588
39874
  const [current] = await tx.select().from(workspaces3).where(eq(workspaces3.id, claim.workspaceId)).for("update");
39589
- if (current?.state !== "queued")
39875
+ if (current?.state !== "queued" || current.purgeRequestedAt)
39590
39876
  return null;
39591
39877
  const counts = await countActive(tx);
39592
39878
  if (counts.global >= claim.limits.globalActiveWorkspaces)
@@ -39706,7 +39992,15 @@ function createTransitions(context) {
39706
39992
  return {
39707
39993
  async updateWorkspace(id, patch, at) {
39708
39994
  const bumpsChange = Object.keys(patch).some((key) => CHANGE_PATCH_KEYS.has(key));
39709
- await db.update(workspaces3).set({ ...patch, updatedAt: at, ...bumpsChange ? { changeSeq: sql`${workspaces3.changeSeq}+1` } : {} }).where(eq(workspaces3.id, id));
39995
+ await db.transaction(async (tx) => {
39996
+ const [current] = await tx.select().from(workspaces3).where(eq(workspaces3.id, id)).for("update");
39997
+ await tx.update(workspaces3).set({
39998
+ ...patch,
39999
+ ...current?.purgeRequestedAt ? purgedContentPatch() : {},
40000
+ updatedAt: at,
40001
+ ...bumpsChange ? { changeSeq: sql`${workspaces3.changeSeq}+1` } : {}
40002
+ }).where(eq(workspaces3.id, id));
40003
+ });
39710
40004
  if (bumpsChange)
39711
40005
  notifyChange(context, id);
39712
40006
  },
@@ -39715,9 +40009,12 @@ function createTransitions(context) {
39715
40009
  const [current] = await tx.select().from(workspaces3).where(eq(workspaces3.id, id)).for("update");
39716
40010
  if (!current || !req.from.includes(current.state) || !canTransition(current.state, req.to))
39717
40011
  return null;
40012
+ if (current.purgeRequestedAt && ["provisioning", "connected", "ready", "preserving", "queued"].includes(req.to))
40013
+ return null;
39718
40014
  const terminal = isTerminal(req.to);
39719
40015
  const [row] = await tx.update(workspaces3).set({
39720
40016
  ...req.patch,
40017
+ ...current.purgeRequestedAt ? purgedContentPatch() : {},
39721
40018
  state: req.to,
39722
40019
  reasonCode: req.reason,
39723
40020
  updatedAt: req.at,
@@ -39748,9 +40045,12 @@ function createTransitions(context) {
39748
40045
 
39749
40046
  // ../db/src/store.ts
39750
40047
  class PostgresStore {
39751
- constructor(databaseUrl, schema = "pocketcoder") {
39752
- const context = createDatabaseContext(databaseUrl, schema);
40048
+ constructor(databaseUrl, schema = "pocketcoder", options = {}) {
40049
+ const context = createDatabaseContext(databaseUrl, schema, options);
39753
40050
  const lifecycle2 = createLifecycle(context);
40051
+ const content = createContentPurge(context);
40052
+ this.listWorkspaceStorage = content.listWorkspaceStorage;
40053
+ this.purgeWorkspaceContent = content.purgeWorkspaceContent;
39754
40054
  this.init = lifecycle2.init;
39755
40055
  this.close = lifecycle2.close;
39756
40056
  this.acquireCoordinatorLease = lifecycle2.acquireCoordinatorLease;
@@ -39765,6 +40065,10 @@ class PostgresStore {
39765
40065
  this.listPrincipals = auth2.listPrincipals;
39766
40066
  this.updatePrincipal = auth2.updatePrincipal;
39767
40067
  this.setPrincipalDisabled = auth2.setPrincipalDisabled;
40068
+ this.getPrincipal = auth2.getPrincipal;
40069
+ this.issueMachineKey = auth2.issueMachineKey;
40070
+ this.listMachineKeys = auth2.listMachineKeys;
40071
+ this.revokePrincipalKeys = auth2.revokePrincipalKeys;
39768
40072
  this.insertMachineKey = auth2.insertMachineKey;
39769
40073
  this.getMachineKeyWithPrincipal = auth2.getMachineKeyWithPrincipal;
39770
40074
  this.revokeMachineKey = auth2.revokeMachineKey;
@@ -39837,6 +40141,8 @@ class PostgresStore {
39837
40141
  this.appendEvent = outbox2.appendEvent;
39838
40142
  this.waitForWorkspaceChange = createWorkspaceWaiter(context, workspaces3.getWorkspace);
39839
40143
  }
40144
+ listWorkspaceStorage;
40145
+ purgeWorkspaceContent;
39840
40146
  init;
39841
40147
  close;
39842
40148
  acquireCoordinatorLease;
@@ -39849,6 +40155,10 @@ class PostgresStore {
39849
40155
  listPrincipals;
39850
40156
  updatePrincipal;
39851
40157
  setPrincipalDisabled;
40158
+ getPrincipal;
40159
+ issueMachineKey;
40160
+ listMachineKeys;
40161
+ revokePrincipalKeys;
39852
40162
  insertMachineKey;
39853
40163
  getMachineKeyWithPrincipal;
39854
40164
  revokeMachineKey;
@@ -40174,6 +40484,51 @@ class OutputsApi extends WorkspaceCursorApi {
40174
40484
  }
40175
40485
  }
40176
40486
 
40487
+ // ../sdk/src/resources/keys/keys.ts
40488
+ class KeysApi {
40489
+ transport;
40490
+ constructor(transport) {
40491
+ this.transport = transport;
40492
+ }
40493
+ list(principalId, query = {}, options = {}) {
40494
+ return this.transport.request(`/v1/principals/${encodeURIComponent(principalId)}/keys?${queryString({ limit: query.limit, cursor: query.cursor, request_id: query.requestId })}`, KeyListResponseSchema, options);
40495
+ }
40496
+ async* all(principalId, query = {}, options = {}) {
40497
+ let cursor;
40498
+ do {
40499
+ const page2 = await this.list(principalId, { ...query, cursor }, options);
40500
+ yield* page2.items;
40501
+ cursor = page2.next_cursor ?? undefined;
40502
+ } while (cursor);
40503
+ }
40504
+ issue(principalId, input, options = {}) {
40505
+ return this.transport.request(`/v1/principals/${encodeURIComponent(principalId)}/keys`, KeyIssueResponseSchema, {
40506
+ method: "POST",
40507
+ body: JSON.stringify(input),
40508
+ signal: options.signal
40509
+ });
40510
+ }
40511
+ revoke(principalId, keyId, options = {}) {
40512
+ return this.transport.request(`/v1/principals/${encodeURIComponent(principalId)}/keys/${encodeURIComponent(keyId)}`, exports_external.object({ revoked: exports_external.literal(true) }), { method: "DELETE", signal: options.signal });
40513
+ }
40514
+ revokeAll(principalId, options = {}) {
40515
+ return this.transport.request(`/v1/principals/${encodeURIComponent(principalId)}/keys`, exports_external.object({ revoked: exports_external.literal(true) }), { method: "DELETE", signal: options.signal });
40516
+ }
40517
+ }
40518
+
40519
+ class RecoveryApi {
40520
+ transport;
40521
+ constructor(transport) {
40522
+ this.transport = transport;
40523
+ }
40524
+ purge(principalId, workspaceId, executionKey, options = {}) {
40525
+ return this.transport.request(`/v1/principals/${encodeURIComponent(principalId)}/workspaces/${encodeURIComponent(workspaceId)}/purge`, OperationResourceSchema, { method: "POST", headers: { "Idempotency-Key": executionKey }, body: "{}", signal: options.signal });
40526
+ }
40527
+ operation(principalId, operationId, options = {}) {
40528
+ return this.transport.request(`/v1/principals/${encodeURIComponent(principalId)}/operations/${encodeURIComponent(operationId)}`, OperationResourceSchema, options);
40529
+ }
40530
+ }
40531
+
40177
40532
  // ../sdk/src/resources/templates/templates.ts
40178
40533
  class TemplatesApi {
40179
40534
  transport;
@@ -40383,6 +40738,9 @@ class WorkspacesApi {
40383
40738
  preserve(id, input, key, options = {}) {
40384
40739
  return this.jsonOperation(`/v1/workspaces/${encodeURIComponent(id)}/preserve`, input, key, PreserveResponseSchema, options);
40385
40740
  }
40741
+ purge(id, key, options = {}) {
40742
+ return this.jsonOperation(`/v1/workspaces/${encodeURIComponent(id)}/purge`, {}, key, OperationResourceSchema, options);
40743
+ }
40386
40744
  recreate(id, input, key, options = {}) {
40387
40745
  return this.jsonOperation(`/v1/workspaces/${encodeURIComponent(id)}/recreate`, input, key, RestoreResponseSchema, options);
40388
40746
  }
@@ -40544,6 +40902,8 @@ class PocketCoderClient {
40544
40902
  outputs;
40545
40903
  administration;
40546
40904
  terminals;
40905
+ keys;
40906
+ recovery;
40547
40907
  constructor(config2, fetchImpl = fetch) {
40548
40908
  this.transport = new PocketCoderTransport(config2, fetchImpl);
40549
40909
  this.templates = new TemplatesApi(this.transport);
@@ -40558,6 +40918,8 @@ class PocketCoderClient {
40558
40918
  this.outputs = new OutputsApi(this.transport);
40559
40919
  this.administration = new AdministrationApi(this.transport);
40560
40920
  this.terminals = new TerminalsApi(this.transport);
40921
+ this.keys = new KeysApi(this.transport);
40922
+ this.recovery = new RecoveryApi(this.transport);
40561
40923
  }
40562
40924
  raw(path, init = {}) {
40563
40925
  return this.transport.raw(path, init);
@@ -40803,7 +41165,7 @@ async function api2(path, init = {}) {
40803
41165
  }
40804
41166
  async function withStore(fn) {
40805
41167
  const { url: url2, schema } = dbConfig();
40806
- const store = new PostgresStore(url2, schema);
41168
+ const store = new PostgresStore(url2, schema, { max: 1 });
40807
41169
  try {
40808
41170
  return await fn(store);
40809
41171
  } finally {
@@ -41062,66 +41424,103 @@ async function printFailureTail(workspaceId) {
41062
41424
  } catch {}
41063
41425
  }
41064
41426
 
41427
+ // src/commands/keys/issue.ts
41428
+ import { randomUUID as randomUUID11 } from "crypto";
41429
+
41065
41430
  // src/commands/scopes.ts
41066
41431
  function parseScopes(value) {
41067
- const items = valueList(value);
41068
- for (const item of items)
41432
+ return valueList(value).map((item) => {
41069
41433
  if (!isScope(item))
41070
41434
  fail(`unknown scope: ${item}`);
41071
- return items;
41435
+ return item;
41436
+ });
41072
41437
  }
41073
41438
 
41074
41439
  // src/commands/keys/issue.ts
41075
41440
  function addIssueCommand(parser2) {
41076
- return addAction(parser2, "issue", "Issue a machine key", (command2) => command2.option("principal", {
41077
- type: "string",
41078
- demandOption: true,
41079
- description: "Principal name"
41080
- }).option("scopes", {
41441
+ return addAction(parser2, "issue", "Issue or reconcile a machine key", (command2) => command2.option("principal", { type: "string", description: "Principal name for local operator bootstrap" }).option("principal-id", { type: "string", description: "Target principal ID through the public API" }).option("request-id", {
41081
41442
  type: "string",
41082
- description: "Comma-separated scopes; defaults to the principal scopes"
41083
- }).option("expires", {
41443
+ description: "Persist this identity before issuance to reconcile a lost response"
41444
+ }).option("scopes", { type: "string", description: "Comma-separated restricted scopes" }).option("expires", {
41084
41445
  type: "string",
41085
41446
  default: "never",
41086
- description: "Expiration as ISO 8601, or never"
41447
+ description: "ISO 8601 expiry; never is local operator only"
41448
+ }).option("manage-principals", {
41449
+ type: "string",
41450
+ description: "Explicit target UUIDs for local operator bootstrap"
41451
+ }).option("json", {
41452
+ type: "boolean",
41453
+ default: false,
41454
+ description: "Print key metadata and one-time token as JSON"
41455
+ }).conflicts("principal", "principal-id").implies("principal-id", "request-id").check((flags) => {
41456
+ if (!flags.principal && !flags["principal-id"])
41457
+ throw new Error("Missing required argument: principal or principal-id");
41458
+ return true;
41087
41459
  }), async (flags) => {
41460
+ const expires = need(flags, "expires");
41461
+ const expiresAt = expires === "never" ? null : expires;
41462
+ const scopes = typeof flags.scopes === "string" ? parseScopes(flags.scopes) : [];
41463
+ const requestId = typeof flags["request-id"] === "string" ? flags["request-id"] : randomUUID11();
41464
+ if (typeof flags["principal-id"] === "string") {
41465
+ if (!expiresAt)
41466
+ fail("public key issuance requires --expires <ISO8601>");
41467
+ if (flags["manage-principals"])
41468
+ fail("delegated grants require local operator bootstrap");
41469
+ const result = await controlPlaneClient().keys.issue(flags["principal-id"], {
41470
+ request_id: requestId,
41471
+ scopes,
41472
+ expires_at: expiresAt
41473
+ });
41474
+ console.log(JSON.stringify(result, null, 2));
41475
+ return;
41476
+ }
41088
41477
  const pepper = process.env.POCKETCODER_AUTH_PEPPER;
41089
41478
  if (!pepper)
41090
- fail("POCKETCODER_AUTH_PEPPER is required to issue keys");
41479
+ fail("POCKETCODER_AUTH_PEPPER is required to issue keys locally");
41091
41480
  await withStore(async (store) => {
41092
41481
  const name2 = need(flags, "principal");
41093
41482
  const principal = await store.getPrincipalByName(name2);
41094
41483
  if (!principal)
41095
41484
  fail(`unknown principal: ${name2}`);
41096
- const expiresRaw = typeof flags.expires === "string" ? flags.expires : "never";
41097
- const expiresAt = expiresRaw === "never" ? null : new Date(expiresRaw);
41098
- if (expiresAt && Number.isNaN(expiresAt.getTime())) {
41099
- fail(`invalid --expires value: ${expiresRaw}`);
41100
- }
41101
- const key = issueMachineKey(pepper);
41102
- await store.insertMachineKey({
41103
- id: key.id,
41104
- principalId: principal.id,
41105
- secretDigest: key.secretDigest,
41106
- scopes: typeof flags.scopes === "string" ? parseScopes(flags.scopes) : [],
41107
- createdAt: new Date,
41108
- expiresAt,
41109
- revokedAt: null,
41110
- lastUsedAt: null
41111
- });
41112
- console.log("machine key (shown once, store it now):");
41113
- console.log(key.token);
41485
+ const managedPrincipalIds = typeof flags["manage-principals"] === "string" ? valueList(flags["manage-principals"]) : [];
41486
+ for (const id of managedPrincipalIds) {
41487
+ if (!/^[0-9a-f-]{36}$/i.test(id) || !await store.getPrincipal(id))
41488
+ fail("unknown managed principal ID");
41489
+ }
41490
+ const result = await issuePrincipalKey(store, pepper, principal, { request_id: requestId, scopes, expires_at: expiresAt }, { managedPrincipalIds, operatorBootstrap: true });
41491
+ if (flags.json || !result.token)
41492
+ console.log(JSON.stringify({ key: result.key, token: result.token }, null, 2));
41493
+ else {
41494
+ console.log(`machine key (shown once; request ${requestId}):`);
41495
+ console.log(result.token);
41496
+ }
41114
41497
  });
41115
41498
  });
41116
41499
  }
41117
41500
 
41501
+ // src/commands/keys/list.ts
41502
+ function addListCommand2(parser2) {
41503
+ return addAction(parser2, "list", "List authoritative key metadata through the public API", (command2) => command2.option("principal-id", { type: "string", demandOption: true }).option("request-id", { type: "string", description: "Reconcile an issuance request" }).option("cursor", { type: "string" }).option("limit", { type: "number", default: 50 }), async (flags) => {
41504
+ const result = await controlPlaneClient().keys.list(need(flags, "principal-id"), {
41505
+ limit: Number(flags.limit),
41506
+ ...typeof flags.cursor === "string" ? { cursor: flags.cursor } : {},
41507
+ ...typeof flags["request-id"] === "string" ? { requestId: flags["request-id"] } : {}
41508
+ });
41509
+ console.log(JSON.stringify(result, null, 2));
41510
+ });
41511
+ }
41512
+
41118
41513
  // src/commands/keys/revoke.ts
41119
41514
  function addRevokeCommand(parser2) {
41120
- return addAction(parser2, "revoke", "Revoke a machine key", (command2) => command2.option("id", {
41515
+ return addAction(parser2, "revoke", "Revoke a machine key", (command2) => command2.option("principal-id", { type: "string", description: "Target principal ID through the public API" }).option("id", {
41121
41516
  type: "string",
41122
41517
  demandOption: true,
41123
41518
  description: "Machine key ID"
41124
41519
  }), async (flags) => {
41520
+ if (typeof flags["principal-id"] === "string") {
41521
+ console.log(JSON.stringify(await controlPlaneClient().keys.revoke(flags["principal-id"], need(flags, "id")), null, 2));
41522
+ return;
41523
+ }
41125
41524
  await withStore(async (store) => {
41126
41525
  const id = need(flags, "id");
41127
41526
  const revoked = await store.revokeMachineKey(id, new Date);
@@ -41130,13 +41529,20 @@ function addRevokeCommand(parser2) {
41130
41529
  });
41131
41530
  }
41132
41531
 
41532
+ // src/commands/keys/revoke-all.ts
41533
+ function addRevokeAllCommand(parser2) {
41534
+ return addAction(parser2, "revoke-all", "Disable a principal and revoke every key atomically", (command2) => command2.option("principal-id", { type: "string", demandOption: true }), async (flags) => {
41535
+ console.log(JSON.stringify(await controlPlaneClient().keys.revokeAll(need(flags, "principal-id")), null, 2));
41536
+ });
41537
+ }
41538
+
41133
41539
  // src/commands/keys/index.ts
41134
41540
  function addKeyCommands(parser2) {
41135
- return addResource(parser2, "keys", "Manage machine keys", (commands) => addRevokeCommand(addIssueCommand(commands)));
41541
+ return addResource(parser2, "keys", "Manage machine keys", (commands) => [addIssueCommand, addListCommand2, addRevokeCommand, addRevokeAllCommand].reduce((parser3, add) => add(parser3), commands));
41136
41542
  }
41137
41543
 
41138
41544
  // src/commands/pools/list.ts
41139
- function addListCommand2(parser2) {
41545
+ function addListCommand3(parser2) {
41140
41546
  return addAction(parser2, "list", "List warm pool inventory and metrics", (command2) => command2.option("json", { type: "boolean", description: "Print JSON" }), async (flags) => {
41141
41547
  const body = await controlPlaneClient().administration.warmPools();
41142
41548
  if (flags.json)
@@ -41153,7 +41559,7 @@ function addListCommand2(parser2) {
41153
41559
 
41154
41560
  // src/commands/pools/index.ts
41155
41561
  function addPoolCommands(parser2) {
41156
- return addResource(parser2, "pools", "Inspect operator-managed warm capacity", addListCommand2);
41562
+ return addResource(parser2, "pools", "Inspect operator-managed warm capacity", addListCommand3);
41157
41563
  }
41158
41564
 
41159
41565
  // src/commands/principals/options.ts
@@ -41179,7 +41585,7 @@ function addCreateCommand(parser2) {
41179
41585
  }
41180
41586
 
41181
41587
  // src/commands/principals/list.ts
41182
- function addListCommand3(parser2) {
41588
+ function addListCommand4(parser2) {
41183
41589
  return addAction(parser2, "list", "List principals", unchanged, async () => {
41184
41590
  await withStore(async (store) => {
41185
41591
  for (const principal of await store.listPrincipals()) {
@@ -41208,21 +41614,72 @@ function addUpdateCommand(parser2) {
41208
41614
 
41209
41615
  // src/commands/principals/index.ts
41210
41616
  function addPrincipalCommands(parser2) {
41211
- return addResource(parser2, "principals", "Manage principals", (commands) => addListCommand3(addUpdateCommand(addCreateCommand(commands))));
41617
+ return addResource(parser2, "principals", "Manage principals", (commands) => addListCommand4(addUpdateCommand(addCreateCommand(commands))));
41212
41618
  }
41213
41619
 
41214
41620
  // src/commands/server/process.ts
41215
41621
  import { spawn, spawnSync } from "child_process";
41216
- import { createHash as createHash4, randomUUID as randomUUID26 } from "crypto";
41217
- import { closeSync, existsSync as existsSync4, mkdirSync, openSync, readFileSync as readFileSync4, renameSync, rmSync, writeFileSync } from "fs";
41622
+ import { createHash as createHash4, randomUUID as randomUUID28 } from "crypto";
41623
+ import { closeSync, existsSync as existsSync4, mkdirSync, openSync, readFileSync as readFileSync6, renameSync, rmSync, writeFileSync } from "fs";
41218
41624
  import { homedir } from "os";
41219
41625
  import { dirname as dirname7, join as join8, resolve as resolve11 } from "path";
41220
41626
 
41221
41627
  // ../server/src/config/config.ts
41222
41628
  import { randomBytes as randomBytes2 } from "crypto";
41223
41629
 
41630
+ // ../server/src/lifecycle/launch-policy.ts
41631
+ import { readFileSync as readFileSync4 } from "fs";
41632
+ function launchPolicyConfig(env2) {
41633
+ const url2 = env2.POCKETCODER_LAUNCH_POLICY_URL;
41634
+ const tokenFile = env2.POCKETCODER_LAUNCH_POLICY_TOKEN_FILE;
41635
+ if (!url2 && !tokenFile)
41636
+ return;
41637
+ if (!url2 || !tokenFile)
41638
+ throw new Error("Launch policy requires both URL and token file");
41639
+ const parsed = new URL(url2);
41640
+ const loopback = parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
41641
+ if (parsed.username || parsed.password || parsed.hash || parsed.search || parsed.protocol !== "https:" && !(parsed.protocol === "http:" && loopback)) {
41642
+ throw new Error("Launch policy requires HTTPS or a loopback HTTP endpoint");
41643
+ }
41644
+ return { url: url2, tokenFile };
41645
+ }
41646
+ function createLaunchPolicy(url2, token) {
41647
+ return async (workspace) => {
41648
+ try {
41649
+ const response = await fetch(url2, {
41650
+ method: "POST",
41651
+ redirect: "error",
41652
+ signal: AbortSignal.timeout(5000),
41653
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
41654
+ body: JSON.stringify({
41655
+ workspace_id: workspace.id,
41656
+ principal_id: workspace.principalId,
41657
+ external_id: workspace.externalId,
41658
+ template_name: workspace.templateName,
41659
+ launch_mode: workspace.launchMode,
41660
+ metadata: workspace.metadata
41661
+ })
41662
+ });
41663
+ if (response.status !== 200)
41664
+ return false;
41665
+ const decision = await response.json();
41666
+ return typeof decision === "object" && decision !== null && "allowed" in decision && decision.allowed === true && "workspace_id" in decision && decision.workspace_id === workspace.id;
41667
+ } catch {
41668
+ return false;
41669
+ }
41670
+ };
41671
+ }
41672
+ function loadLaunchPolicy(config2) {
41673
+ if (!config2)
41674
+ return;
41675
+ const token = readFileSync4(config2.tokenFile, "utf8").trim();
41676
+ if (token.length < 24 || /\s/.test(token))
41677
+ throw new Error("Invalid launch policy control token");
41678
+ return createLaunchPolicy(config2.url, token);
41679
+ }
41680
+
41224
41681
  // ../server/src/persistence/persistence-base.ts
41225
- import { randomUUID as randomUUID11 } from "crypto";
41682
+ import { randomUUID as randomUUID12 } from "crypto";
41226
41683
  var DEFAULT_PERSISTENCE_LIMITS = {
41227
41684
  maxRetainedBytes: 500 * 1024 ** 3,
41228
41685
  maxRetainedBytesPerPrincipal: 100 * 1024 ** 3,
@@ -41365,7 +41822,7 @@ class PersistenceContext {
41365
41822
  async emitCheckpointEvent(type, row) {
41366
41823
  const at = this.now();
41367
41824
  await this.deps.store.appendEvent(row.workspaceId, type, {
41368
- id: randomUUID11(),
41825
+ id: randomUUID12(),
41369
41826
  type,
41370
41827
  occurred_at: at.toISOString(),
41371
41828
  checkpoint: {
@@ -41381,7 +41838,7 @@ class PersistenceContext {
41381
41838
  }
41382
41839
  }
41383
41840
  // ../server/src/persistence/persistence-checkpoints.ts
41384
- import { randomUUID as randomUUID12 } from "crypto";
41841
+ import { randomUUID as randomUUID13 } from "crypto";
41385
41842
  class PersistenceCheckpointService {
41386
41843
  context;
41387
41844
  constructor(context) {
@@ -41400,7 +41857,7 @@ class PersistenceCheckpointService {
41400
41857
  }
41401
41858
  const now = this.context.now();
41402
41859
  const inserted = await this.context.insertOperation({
41403
- id: randomUUID12(),
41860
+ id: randomUUID13(),
41404
41861
  principalId: principal.id,
41405
41862
  kind: "verify",
41406
41863
  state: "running",
@@ -41479,7 +41936,7 @@ class PersistenceCheckpointService {
41479
41936
  });
41480
41937
  const at = this.context.now();
41481
41938
  await this.context.deps.store.appendEvent(workspaceId, "workspace.output_published", {
41482
- id: randomUUID12(),
41939
+ id: randomUUID13(),
41483
41940
  type: "workspace.output_published",
41484
41941
  occurred_at: at.toISOString(),
41485
41942
  workspace_id: workspaceId,
@@ -41490,7 +41947,7 @@ class PersistenceCheckpointService {
41490
41947
  }
41491
41948
 
41492
41949
  // ../server/src/persistence/persistence-maintenance.ts
41493
- import { randomUUID as randomUUID13 } from "crypto";
41950
+ import { randomUUID as randomUUID14 } from "crypto";
41494
41951
 
41495
41952
  // ../server/src/persistence/persistence-cleanup.ts
41496
41953
  async function cleanupPreservedStorage(context, storageId) {
@@ -41624,7 +42081,7 @@ class PersistenceMaintenanceService {
41624
42081
  }
41625
42082
  const now = this.context.now();
41626
42083
  const inserted = await this.context.insertOperation({
41627
- id: randomUUID13(),
42084
+ id: randomUUID14(),
41628
42085
  principalId: principal.id,
41629
42086
  kind: "delete",
41630
42087
  state: "running",
@@ -41666,7 +42123,7 @@ class PersistenceMaintenanceService {
41666
42123
  }
41667
42124
 
41668
42125
  // ../server/src/persistence/persistence-preserve.ts
41669
- import { randomUUID as randomUUID14 } from "crypto";
42126
+ import { randomUUID as randomUUID15 } from "crypto";
41670
42127
  class PreservePersistenceService {
41671
42128
  context;
41672
42129
  preserveRunner;
@@ -41692,8 +42149,8 @@ class PreservePersistenceService {
41692
42149
  throw new ApiError("validation.invalid", "retention may not exceed the template checkpoint policy");
41693
42150
  }
41694
42151
  this.context.storageDriver();
41695
- const checkpointId = randomUUID14();
41696
- const operationId = randomUUID14();
42152
+ const checkpointId = randomUUID15();
42153
+ const operationId = randomUUID15();
41697
42154
  const now = this.context.now();
41698
42155
  const operationResult = await this.context.insertOperation({
41699
42156
  id: operationId,
@@ -41809,11 +42266,7 @@ class PersistencePreserveRunner {
41809
42266
  const quiesced = native || hook ? await this.context.deps.hub.prepareCheckpoint(workspace.id, operationId, deadlineMs) : false;
41810
42267
  if (!workspace.providerRef)
41811
42268
  return quiesced;
41812
- await this.context.deps.driver.stop({
41813
- kind: workspace.providerKind ?? "",
41814
- id: "",
41815
- ...workspace.providerRef
41816
- }, Math.max(1, Math.ceil(parseDurationMs(workspace.templateSnapshot.spec.timeouts.terminateGrace) / 1000)));
42269
+ await stopWorkspaceProvider(this.context.deps.store, this.context.deps.driver, workspace, Math.max(1, Math.ceil(parseDurationMs(workspace.templateSnapshot.spec.timeouts.terminateGrace) / 1000)), this.context.now(), false);
41817
42270
  return quiesced;
41818
42271
  }
41819
42272
  async createSnapshot(workspace, checkpoint, storage) {
@@ -41922,11 +42375,211 @@ class PersistencePreserveRunner {
41922
42375
  }
41923
42376
  }
41924
42377
 
41925
- // ../server/src/persistence/persistence-restore.ts
42378
+ // ../server/src/persistence/persistence-purge.ts
41926
42379
  import { randomUUID as randomUUID16 } from "crypto";
41927
42380
 
42381
+ // ../server/src/persistence/purge-storage.ts
42382
+ class PurgeOwnershipError extends Error {
42383
+ }
42384
+ function storageTargets(workspace, allocations, physical) {
42385
+ const ids = new Set(allocations.map((row) => row.id));
42386
+ if (physical.some((row) => row.workspaceId === workspace.id && !ids.has(row.storageId))) {
42387
+ throw new PurgeOwnershipError("Unrecorded workspace allocation");
42388
+ }
42389
+ return allocations.map((allocation) => {
42390
+ const found = physical.find((row) => row.storageId === allocation.id);
42391
+ if (allocation.principalId !== workspace.principalId || allocation.state === "quarantined" || found?.workspaceId && found.workspaceId !== workspace.id) {
42392
+ throw new PurgeOwnershipError("Storage ownership is unresolved");
42393
+ }
42394
+ const ref = Object.keys(allocation.providerRef).length ? allocation.providerRef : found?.ref;
42395
+ if (ref && ref.id !== allocation.id)
42396
+ throw new PurgeOwnershipError("Mismatched allocation reference");
42397
+ return { id: allocation.id, ref };
42398
+ });
42399
+ }
42400
+ function checkpointTargets(checkpoints, physical, storageIds) {
42401
+ return checkpoints.map((checkpoint) => {
42402
+ if (!storageIds.has(checkpoint.storageId))
42403
+ throw new PurgeOwnershipError("Checkpoint storage ownership is unresolved");
42404
+ const ref = checkpoint.providerRef ?? physical.find((row) => row.checkpointId === checkpoint.id)?.ref;
42405
+ if (ref && ref.id !== checkpoint.id)
42406
+ throw new PurgeOwnershipError("Mismatched checkpoint reference");
42407
+ return { id: checkpoint.id, ref };
42408
+ });
42409
+ }
42410
+ async function purgeStorage(context, workspace) {
42411
+ const { store, storageDriver } = context.deps;
42412
+ const allocations = await store.listWorkspaceStorage(workspace.id);
42413
+ const checkpoints = await store.listCheckpoints(workspace.principalId, { workspaceId: workspace.id });
42414
+ if (!storageDriver) {
42415
+ if (allocations.length || checkpoints.length || workspace.templateSnapshot.spec.persistence.mounts.length) {
42416
+ throw new Error("Storage driver unavailable");
42417
+ }
42418
+ return;
42419
+ }
42420
+ const physicalStorage = await storageDriver.listStorage();
42421
+ const physicalCheckpoints = await storageDriver.listCheckpoints();
42422
+ for (const physical of physicalStorage) {
42423
+ if (!await store.getStorage(physical.storageId))
42424
+ throw new PurgeOwnershipError("Unrecorded storage requires reconciliation");
42425
+ }
42426
+ for (const physical of physicalCheckpoints) {
42427
+ if (!await store.getCheckpoint(physical.checkpointId))
42428
+ throw new PurgeOwnershipError("Unrecorded checkpoint requires reconciliation");
42429
+ }
42430
+ const allocationIds = new Set(allocations.map((row) => row.id));
42431
+ const storage = storageTargets(workspace, allocations, physicalStorage);
42432
+ const snapshots = checkpointTargets(checkpoints, physicalCheckpoints, allocationIds);
42433
+ for (const checkpoint of snapshots) {
42434
+ if (checkpoint.ref)
42435
+ await storageDriver.deleteCheckpoint(checkpoint.ref);
42436
+ await store.updateCheckpoint(checkpoint.id, { state: "deleted", deletedAt: context.now() }, context.now());
42437
+ }
42438
+ for (const allocation of storage) {
42439
+ if (allocation.ref)
42440
+ await storageDriver.deleteStorage(allocation.ref);
42441
+ await store.updateWorkspaceStorage(allocation.id, { state: "deleted", deletedAt: context.now(), retainedUntil: null, lastErrorCode: null }, context.now());
42442
+ }
42443
+ const checkpointIds = new Set(checkpoints.map((row) => row.id));
42444
+ const remainingStorage = (await storageDriver.listStorage()).some((row) => allocationIds.has(row.storageId) || row.workspaceId === workspace.id);
42445
+ const remainingCheckpoints = (await storageDriver.listCheckpoints()).some((row) => checkpointIds.has(row.checkpointId));
42446
+ if (remainingStorage || remainingCheckpoints)
42447
+ throw new Error("Storage deletion not verified");
42448
+ }
42449
+
42450
+ // ../server/src/persistence/persistence-purge.ts
42451
+ class PersistencePurgeService {
42452
+ context;
42453
+ running = new Map;
42454
+ constructor(context) {
42455
+ this.context = context;
42456
+ }
42457
+ async purge(principal, workspaceId, idempotencyKey) {
42458
+ await this.context.deps.workspaces.getOwned(principal, workspaceId);
42459
+ const at = this.context.now();
42460
+ const result = await this.context.insertOperation({
42461
+ id: randomUUID16(),
42462
+ principalId: principal.id,
42463
+ kind: "purge",
42464
+ state: "pending",
42465
+ idempotencyKey,
42466
+ requestDigest: digestOf({ workspace_id: workspaceId }),
42467
+ workspaceId,
42468
+ checkpointId: null,
42469
+ resultWorkspaceId: null,
42470
+ reasonCode: null,
42471
+ attemptCount: 0,
42472
+ createdAt: at,
42473
+ updatedAt: at,
42474
+ completedAt: null
42475
+ });
42476
+ if (result.conflict)
42477
+ throw new ApiError("idempotency.conflict", "Changed purge request.");
42478
+ if (result.operation.state !== "succeeded")
42479
+ this.run(result.operation).catch(() => {});
42480
+ return result.operation;
42481
+ }
42482
+ async retry() {
42483
+ const operations = await this.context.deps.store.listIncompleteOperations();
42484
+ await Promise.all(operations.filter((operation) => operation.kind === "purge").map((operation) => this.run(operation)));
42485
+ return (await this.context.deps.store.listIncompleteOperations()).filter((operation) => operation.kind === "purge").length;
42486
+ }
42487
+ run(operation) {
42488
+ const workspaceId = operation.workspaceId;
42489
+ if (!workspaceId)
42490
+ return Promise.resolve();
42491
+ const active = this.running.get(workspaceId);
42492
+ if (active)
42493
+ return active;
42494
+ const task = this.attempt(operation, workspaceId).finally(() => this.running.delete(workspaceId));
42495
+ this.running.set(workspaceId, task);
42496
+ return task;
42497
+ }
42498
+ async attempt(operation, workspaceId) {
42499
+ const { store, scheduler, driver, hub } = this.context.deps;
42500
+ let reason = "purge_termination_unresolved";
42501
+ try {
42502
+ await store.updateOperation(operation.id, { state: "running", reasonCode: null, attemptCount: operation.attemptCount + 1 }, this.context.now());
42503
+ await scheduler.drain();
42504
+ const workspace = await store.getWorkspace(workspaceId);
42505
+ if (!workspace)
42506
+ throw new Error("Workspace missing");
42507
+ if (await this.hasActiveCopies(workspaceId, workspace.state === "queued")) {
42508
+ reason = "purge_operation_in_progress";
42509
+ throw new Error(reason);
42510
+ }
42511
+ if (workspace.state === "preserving") {
42512
+ reason = "purge_operation_in_progress";
42513
+ throw new Error(reason);
42514
+ }
42515
+ if (driver.kind === "kubernetes" && (workspace.launchAttempts > 0 || workspace.providerRef)) {
42516
+ await stopWorkspaceProvider(store, driver, workspace, 1, this.context.now(), false);
42517
+ const stopped = await store.getWorkspace(workspaceId);
42518
+ if (!stopped?.providerRef?.terminationEvidence)
42519
+ throw new Error(reason);
42520
+ }
42521
+ hub.close(workspaceId);
42522
+ if (!isTerminal(workspace.state)) {
42523
+ if (workspace.state === "queued") {
42524
+ await store.transition(workspaceId, {
42525
+ from: ["queued"],
42526
+ to: "canceled",
42527
+ reason: "canceled_by_caller",
42528
+ at: this.context.now()
42529
+ });
42530
+ } else {
42531
+ await scheduler.finalize(workspace, "canceled", "canceled_by_caller", this.context.now());
42532
+ if (!(await store.getWorkspace(workspaceId))?.terminalAt)
42533
+ throw new Error(reason);
42534
+ }
42535
+ }
42536
+ const current = await store.getWorkspace(workspaceId);
42537
+ if (!current)
42538
+ throw new Error("Workspace missing");
42539
+ await stopWorkspaceProvider(store, driver, current, 1, this.context.now());
42540
+ const providers = (await driver.list()).filter((provider) => provider.workspaceId === workspaceId);
42541
+ if (providers.length)
42542
+ throw new Error(reason);
42543
+ reason = "purge_storage_unavailable";
42544
+ await driver.purgeInput(workspaceId);
42545
+ await purgeStorage(this.context, workspace);
42546
+ await store.purgeWorkspaceContent(workspaceId, this.context.now());
42547
+ const done = this.context.now();
42548
+ await store.updateOperation(operation.id, { state: "succeeded", reasonCode: null, completedAt: done }, done);
42549
+ } catch (error51) {
42550
+ if (error51 instanceof PurgeOwnershipError)
42551
+ reason = "purge_ownership_unresolved";
42552
+ await store.updateOperation(operation.id, { state: "pending", reasonCode: reason, completedAt: null }, this.context.now());
42553
+ }
42554
+ }
42555
+ async hasActiveCopies(workspaceId, queued) {
42556
+ const operations = await this.context.deps.store.listIncompleteOperations();
42557
+ let active = false;
42558
+ for (const operation of operations) {
42559
+ if (operation.kind === "purge")
42560
+ continue;
42561
+ if (operation.kind === "restore" && operation.resultWorkspaceId) {
42562
+ const target = await this.context.deps.store.getWorkspace(operation.resultWorkspaceId);
42563
+ if (target && isTerminal(target.state)) {
42564
+ await this.context.failOperation(operation.id, "restore_failed");
42565
+ continue;
42566
+ }
42567
+ }
42568
+ if (queued && operation.kind === "restore" && operation.resultWorkspaceId === workspaceId) {
42569
+ await this.context.failOperation(operation.id, "canceled_by_caller");
42570
+ } else if (operation.workspaceId === workspaceId || operation.resultWorkspaceId === workspaceId) {
42571
+ active = true;
42572
+ }
42573
+ }
42574
+ return active;
42575
+ }
42576
+ }
42577
+
42578
+ // ../server/src/persistence/persistence-restore.ts
42579
+ import { randomUUID as randomUUID18 } from "crypto";
42580
+
41928
42581
  // ../server/src/workspaces/service.ts
41929
- import { randomUUID as randomUUID15 } from "crypto";
42582
+ import { randomUUID as randomUUID17 } from "crypto";
41930
42583
  function templateAuthorized(principal, templateName) {
41931
42584
  return principal.templateNames.includes("*") || principal.templateNames.includes(templateName);
41932
42585
  }
@@ -42041,7 +42694,7 @@ class WorkspaceService {
42041
42694
  this.validateCreateInput(template, body);
42042
42695
  const now = this.now();
42043
42696
  const result = await store.insertWorkspace({
42044
- id: randomUUID15(),
42697
+ id: randomUUID17(),
42045
42698
  principalId: principal.id,
42046
42699
  externalId: body.external_id,
42047
42700
  idempotencyKey,
@@ -42150,9 +42803,9 @@ class PersistenceRestoreService {
42150
42803
  }
42151
42804
  this.context.storageDriver();
42152
42805
  const now = this.context.now();
42153
- const resultWorkspaceId = randomUUID16();
42806
+ const resultWorkspaceId = randomUUID18();
42154
42807
  const operationResult = await this.context.insertOperation({
42155
- id: randomUUID16(),
42808
+ id: randomUUID18(),
42156
42809
  principalId: principal.id,
42157
42810
  kind: "restore",
42158
42811
  state: "pending",
@@ -42213,7 +42866,7 @@ class PersistenceRestoreService {
42213
42866
  }
42214
42867
  await this.context.deps.store.updateOperation(operationResult.operation.id, { resultWorkspaceId: inserted.workspace.id }, now);
42215
42868
  await this.context.deps.store.appendEvent(checkpoint.workspaceId, "workspace.restore_queued", {
42216
- id: randomUUID16(),
42869
+ id: randomUUID18(),
42217
42870
  type: "workspace.restore_queued",
42218
42871
  occurred_at: now.toISOString(),
42219
42872
  origin_workspace_id: checkpoint.workspaceId,
@@ -42232,6 +42885,9 @@ class PersistenceRestoreService {
42232
42885
  class PersistenceService {
42233
42886
  constructor(deps) {
42234
42887
  this.context = new PersistenceContext(deps);
42888
+ const purge = new PersistencePurgeService(this.context);
42889
+ this.purge = purge.purge.bind(purge);
42890
+ this.retryPurges = purge.retry.bind(purge);
42235
42891
  this.preserveRunner = new PersistencePreserveRunner(this.context);
42236
42892
  this.preservePersistence = new PreservePersistenceService(this.context, this.preserveRunner);
42237
42893
  this.checkpoint = new PersistenceCheckpointService(this.context);
@@ -42248,6 +42904,8 @@ class PersistenceService {
42248
42904
  this.delete = this.maintenance.delete.bind(this.maintenance);
42249
42905
  this.restore = this.restoreService.restore.bind(this.restoreService);
42250
42906
  }
42907
+ purge;
42908
+ retryPurges;
42251
42909
  restoreService;
42252
42910
  maintenance;
42253
42911
  checkpoint;
@@ -42598,6 +43256,10 @@ class DockerDriver {
42598
43256
  await rm(this.egressInputPath(workspaceId), { force: true });
42599
43257
  }
42600
43258
  }
43259
+ async purgeInput(workspaceId) {
43260
+ await rm(this.inputPath(workspaceId), { force: true });
43261
+ await rm(this.egressInputPath(workspaceId), { force: true });
43262
+ }
42601
43263
  async cleanupInput(workspaceId) {
42602
43264
  await rm(this.inputPath(workspaceId), { force: true });
42603
43265
  }
@@ -42893,7 +43555,7 @@ async function restoreCheckpointContent(checkpointRoot, target, manifest) {
42893
43555
  }
42894
43556
 
42895
43557
  // ../drivers/src/filesystem/filesystem-inventory.ts
42896
- import { open, readdir as readdir3, readFile, rm as rm3 } from "fs/promises";
43558
+ import { readdir as readdir3, readFile, rm as rm3 } from "fs/promises";
42897
43559
  import { join as join6, resolve as resolve8 } from "path";
42898
43560
  var STORAGE_METADATA_FILE = ".pocketcoder-storage.json";
42899
43561
  var CHECKPOINT_METADATA_FILE = ".pocketcoder-checkpoint.json";
@@ -42921,15 +43583,13 @@ async function discoverStorage(rootDirectory) {
42921
43583
  }
42922
43584
  async function discoverCheckpoints(rootDirectory) {
42923
43585
  const result = [];
42924
- for (const id of await readdir3(rootDirectory)) {
43586
+ for (const entry of await readdir3(rootDirectory)) {
43587
+ const id = entry.replace(/^\.creating-/, "");
42925
43588
  if (!/^[0-9a-f-]{36}$/i.test(id))
42926
43589
  continue;
42927
43590
  const root = childOf(rootDirectory, id);
42928
- try {
42929
- const handle = await open(join6(root, CHECKPOINT_METADATA_FILE), "r");
42930
- await handle.close();
43591
+ if (!result.some((row) => row.checkpointId === id))
42931
43592
  result.push({ checkpointId: id, ref: { kind: "filesystem", id, root } });
42932
- } catch {}
42933
43593
  }
42934
43594
  return result;
42935
43595
  }
@@ -43159,6 +43819,7 @@ class FilesystemStorageDriver {
43159
43819
  async deleteCheckpoint(ref) {
43160
43820
  const checkpoint = this.checkpointRef(ref);
43161
43821
  await deleteCheckpointRoot(checkpoint.root);
43822
+ await deleteCheckpointRoot(join7(this.checkpointRoot, `.creating-${checkpoint.id}`));
43162
43823
  }
43163
43824
  async listStorage() {
43164
43825
  await this.initRoots();
@@ -43253,6 +43914,232 @@ function discoveredWarmRuntimes(output, kind, namespace) {
43253
43914
  });
43254
43915
  }
43255
43916
 
43917
+ // ../drivers/src/kubernetes/kubernetes-evidence.ts
43918
+ var EVIDENCE_FINALIZER = "pocketcoder.dev/termination-evidence";
43919
+ var EVIDENCE_ANNOTATION = "pocketcoder.dev/termination-evidence";
43920
+ var NODE_ANNOTATION = "pocketcoder.dev/termination-node";
43921
+ async function patch(run, kind, name2, value) {
43922
+ await run(["patch", kind, name2, "--type=merge", "-p", JSON.stringify(value)]);
43923
+ }
43924
+ async function podsFor(run, name2) {
43925
+ const result = JSON.parse(await run(["get", "pods", "-l", `job-name=${name2}`, "-o", "json"]));
43926
+ return result.items;
43927
+ }
43928
+ function owned(pod, uid) {
43929
+ return pod.metadata.ownerReferences?.some((owner) => owner.uid === uid && owner.kind === "Job" && owner.controller);
43930
+ }
43931
+ function stopped(pod) {
43932
+ const groups = [
43933
+ [pod.spec.containers, pod.status?.containerStatuses],
43934
+ [pod.spec.initContainers, pod.status?.initContainerStatuses],
43935
+ [pod.spec.ephemeralContainers, pod.status?.ephemeralContainerStatuses]
43936
+ ];
43937
+ if (!pod.spec.containers?.length)
43938
+ return false;
43939
+ if (!pod.spec.nodeName && pod.metadata.deletionTimestamp)
43940
+ return groups.every(([, statuses]) => !statuses?.length);
43941
+ return groups.every(([spec, statuses]) => (spec?.length ?? 0) === (statuses?.length ?? 0) && (spec ?? []).every((container) => {
43942
+ const status = statuses?.find((candidate) => candidate.name === container.name);
43943
+ return status?.containerID && status.state.terminated?.containerID === status.containerID && !["ContainerStatusUnknown", "NodeLost"].includes(status.state.terminated?.reason ?? "") && status.state.terminated?.finishedAt && Number.isInteger(status.state.terminated.exitCode);
43944
+ }));
43945
+ }
43946
+ function podEvidence(pod) {
43947
+ return {
43948
+ metadata: {
43949
+ uid: pod.metadata.uid,
43950
+ ownerReferences: pod.metadata.ownerReferences,
43951
+ deletionTimestamp: pod.metadata.deletionTimestamp
43952
+ },
43953
+ spec: {
43954
+ nodeName: pod.spec.nodeName,
43955
+ containers: pod.spec.containers?.map(({ name: name2 }) => ({ name: name2 })),
43956
+ initContainers: pod.spec.initContainers?.map(({ name: name2 }) => ({ name: name2 })),
43957
+ ephemeralContainers: pod.spec.ephemeralContainers?.map(({ name: name2 }) => ({ name: name2 }))
43958
+ },
43959
+ status: {
43960
+ containerStatuses: statusEvidence(pod.status?.containerStatuses),
43961
+ initContainerStatuses: statusEvidence(pod.status?.initContainerStatuses),
43962
+ ephemeralContainerStatuses: statusEvidence(pod.status?.ephemeralContainerStatuses)
43963
+ }
43964
+ };
43965
+ }
43966
+ function statusEvidence(statuses) {
43967
+ return statuses?.map((status) => ({
43968
+ name: status.name,
43969
+ containerID: status.containerID,
43970
+ state: {
43971
+ terminated: {
43972
+ exitCode: status.state.terminated?.exitCode,
43973
+ finishedAt: status.state.terminated?.finishedAt,
43974
+ containerID: status.state.terminated?.containerID,
43975
+ reason: status.state.terminated?.reason
43976
+ }
43977
+ }
43978
+ }));
43979
+ }
43980
+ async function updatePod(run, pod, update) {
43981
+ const uid = pod.metadata.uid;
43982
+ const owner = pod.metadata.ownerReferences?.find((item) => item.kind === "Job" && item.controller)?.uid;
43983
+ for (let attempt = 0;; attempt++) {
43984
+ try {
43985
+ return await update(pod);
43986
+ } catch (error51) {
43987
+ if (attempt >= 4 || !(error51 instanceof Error) || !error51.message.includes("Error from server (Conflict)"))
43988
+ throw error51;
43989
+ await Bun.sleep(25 * (attempt + 1));
43990
+ pod = JSON.parse(await run(["get", "pod", pod.metadata.name, "-o", "json"]));
43991
+ if (!uid || pod.metadata.uid !== uid || !owner || !owned(pod, owner))
43992
+ throw new Error("Termination provider changed");
43993
+ }
43994
+ }
43995
+ }
43996
+ async function retain(run, pod) {
43997
+ return updatePod(run, pod, (current) => retainCurrent(run, current));
43998
+ }
43999
+ async function retainCurrent(run, pod) {
44000
+ if (!pod.metadata.name || !pod.metadata.uid)
44001
+ throw new Error("Termination evidence unavailable");
44002
+ if (!pod.spec.nodeName) {
44003
+ await patch(run, "pod", pod.metadata.name, {
44004
+ metadata: {
44005
+ uid: pod.metadata.uid,
44006
+ resourceVersion: pod.metadata.resourceVersion,
44007
+ finalizers: [...new Set([...pod.metadata.finalizers ?? [], EVIDENCE_FINALIZER])]
44008
+ }
44009
+ });
44010
+ return null;
44011
+ }
44012
+ const cached2 = pod.metadata.annotations?.[NODE_ANNOTATION];
44013
+ const node = cached2 ? JSON.parse(cached2) : JSON.parse(await run(["get", "node", pod.spec.nodeName, "-o", "json"]));
44014
+ if (!node.metadata.uid || !node.spec.providerID)
44015
+ throw new Error("Termination node identity unavailable");
44016
+ const identity = {
44017
+ metadata: { uid: node.metadata.uid, deletionTimestamp: node.metadata.deletionTimestamp },
44018
+ spec: { providerID: node.spec.providerID },
44019
+ status: { conditions: node.status?.conditions?.filter((condition) => condition.type === "Ready") }
44020
+ };
44021
+ await patch(run, "pod", pod.metadata.name, {
44022
+ metadata: {
44023
+ uid: pod.metadata.uid,
44024
+ resourceVersion: pod.metadata.resourceVersion,
44025
+ finalizers: [...new Set([...pod.metadata.finalizers ?? [], EVIDENCE_FINALIZER])],
44026
+ annotations: { [NODE_ANNOTATION]: JSON.stringify(identity) }
44027
+ }
44028
+ });
44029
+ return identity;
44030
+ }
44031
+ async function retainNodeIdentities(run, name2, jobUid) {
44032
+ const pods = await podsFor(run, name2);
44033
+ if (pods.some((pod) => !owned(pod, jobUid)))
44034
+ throw new Error("Termination provider changed");
44035
+ for (const pod of pods) {
44036
+ if (pod.spec.nodeName && !pod.metadata.annotations?.[NODE_ANNOTATION])
44037
+ await retain(run, pod);
44038
+ }
44039
+ }
44040
+ async function retainNodes(run, pods, nodes) {
44041
+ for (const pod of pods) {
44042
+ if (pod.spec.nodeName && nodes[pod.spec.nodeName] && pod.metadata.finalizers?.includes(EVIDENCE_FINALIZER))
44043
+ continue;
44044
+ const node = await retain(run, pod);
44045
+ if (pod.spec.nodeName)
44046
+ nodes[pod.spec.nodeName] = node;
44047
+ }
44048
+ }
44049
+ async function releasePods(run, name2, jobUid) {
44050
+ for (const pod of await podsFor(run, name2)) {
44051
+ if (!owned(pod, jobUid) || !pod.metadata.finalizers?.includes(EVIDENCE_FINALIZER))
44052
+ continue;
44053
+ await updatePod(run, pod, (current) => patch(run, "pod", current.metadata.name, {
44054
+ metadata: {
44055
+ uid: current.metadata.uid,
44056
+ resourceVersion: current.metadata.resourceVersion,
44057
+ finalizers: current.metadata.finalizers?.filter((item) => item !== EVIDENCE_FINALIZER) ?? []
44058
+ }
44059
+ }));
44060
+ }
44061
+ }
44062
+ async function stoppedJob(run, name2, uid, deadline) {
44063
+ while (true) {
44064
+ const job = JSON.parse(await run(["get", "job", name2, "-o", "json"]));
44065
+ if (job.metadata.uid !== uid)
44066
+ throw new Error("Termination provider changed");
44067
+ const conditions = job.status?.conditions ?? [];
44068
+ const terminal = conditions.some((item) => ["Complete", "Failed"].includes(item.type) && item.status === "True");
44069
+ const suspended = job.spec.suspend && conditions.some((item) => item.type === "Suspended" && item.status === "True");
44070
+ if (terminal || suspended)
44071
+ return job;
44072
+ if (Date.now() >= deadline)
44073
+ throw new Error("Job suspension unconfirmed");
44074
+ await Bun.sleep(100);
44075
+ }
44076
+ }
44077
+ async function captureTermination(run, name2, graceSeconds) {
44078
+ const output = await run(["get", "job", name2, "--ignore-not-found", "-o", "json"]);
44079
+ if (!output)
44080
+ return;
44081
+ const job = JSON.parse(output);
44082
+ if (!job.metadata.uid)
44083
+ throw new Error("Termination evidence unavailable");
44084
+ if (job.metadata.annotations?.[EVIDENCE_ANNOTATION]) {
44085
+ await releasePods(run, name2, job.metadata.uid);
44086
+ return;
44087
+ }
44088
+ const initial = await podsFor(run, name2);
44089
+ if (!initial.length || initial.some((pod) => !owned(pod, job.metadata.uid)))
44090
+ throw new Error("Termination evidence unavailable");
44091
+ const nodes = {};
44092
+ await retainNodes(run, initial, nodes);
44093
+ await patch(run, "job", name2, { metadata: { uid: job.metadata.uid }, spec: { suspend: true } });
44094
+ const confirmed = await stoppedJob(run, name2, job.metadata.uid, Date.now() + (graceSeconds + 5) * 1000);
44095
+ const retained = await podsFor(run, name2);
44096
+ if (retained.length !== initial.length || retained.some((pod) => !initial.some((old) => old.metadata.uid === pod.metadata.uid)))
44097
+ throw new Error("Termination provider changed");
44098
+ await run([
44099
+ "delete",
44100
+ "pod",
44101
+ "-l",
44102
+ `job-name=${name2}`,
44103
+ `--grace-period=${graceSeconds}`,
44104
+ "--wait=false",
44105
+ "--ignore-not-found"
44106
+ ]);
44107
+ const deadline = Date.now() + (graceSeconds + 5) * 1000;
44108
+ while (true) {
44109
+ const pods = await podsFor(run, name2);
44110
+ if (pods.length !== retained.length || pods.some((pod) => !retained.some((old) => old.metadata.uid === pod.metadata.uid)))
44111
+ throw new Error("Termination provider disappeared without evidence");
44112
+ await retainNodes(run, pods, nodes);
44113
+ if (pods.every(stopped)) {
44114
+ const proof = {
44115
+ job: {
44116
+ metadata: { uid: job.metadata.uid, labels: job.metadata.labels },
44117
+ spec: { suspend: confirmed.spec.suspend },
44118
+ status: { conditions: confirmed.status?.conditions }
44119
+ },
44120
+ pods: pods.map(podEvidence),
44121
+ nodes
44122
+ };
44123
+ await patch(run, "job", name2, {
44124
+ metadata: { uid: job.metadata.uid, annotations: { [EVIDENCE_ANNOTATION]: JSON.stringify(proof) } }
44125
+ });
44126
+ await releasePods(run, name2, job.metadata.uid);
44127
+ return;
44128
+ }
44129
+ if (Date.now() >= deadline)
44130
+ throw new Error("Termination evidence unavailable");
44131
+ await Bun.sleep(100);
44132
+ }
44133
+ }
44134
+ async function readTerminationEvidence(run, name2) {
44135
+ const output = await run(["get", "job", name2, "--ignore-not-found", "-o", "json"]);
44136
+ if (!output)
44137
+ return null;
44138
+ const job = JSON.parse(output);
44139
+ const value = job.metadata.annotations?.[EVIDENCE_ANNOTATION];
44140
+ return value ? JSON.parse(value) : null;
44141
+ }
44142
+
43256
44143
  // ../drivers/src/kubernetes/kubernetes-scheduling.ts
43257
44144
  function schedulingFields(options) {
43258
44145
  return {
@@ -43401,7 +44288,7 @@ function workspaceJobManifest(launch, name2, inputSecret, egressSecret, options)
43401
44288
  backoffLimit: 0,
43402
44289
  ttlSecondsAfterFinished: 3600,
43403
44290
  template: {
43404
- metadata: { labels, annotations },
44291
+ metadata: { labels, annotations, ...options.podFinalizers ? { finalizers: options.podFinalizers } : {} },
43405
44292
  spec: {
43406
44293
  restartPolicy: "Never",
43407
44294
  automountServiceAccountToken: false,
@@ -43468,7 +44355,7 @@ function warmJobManifest(launch, name2, inputSecret, egressSecret, options) {
43468
44355
  backoffLimit: 0,
43469
44356
  ttlSecondsAfterFinished: 3600,
43470
44357
  template: {
43471
- metadata: { labels, annotations },
44358
+ metadata: { labels, annotations, ...options.podFinalizers ? { finalizers: options.podFinalizers } : {} },
43472
44359
  spec: {
43473
44360
  restartPolicy: "Never",
43474
44361
  automountServiceAccountToken: false,
@@ -43518,6 +44405,22 @@ function warmJobManifest(launch, name2, inputSecret, egressSecret, options) {
43518
44405
  };
43519
44406
  }
43520
44407
 
44408
+ // ../drivers/src/kubernetes/kubernetes-stop.ts
44409
+ async function stopKubernetesJob(run, name2, graceSeconds) {
44410
+ const job = await run(["get", "job", name2, "--ignore-not-found", "-o", "name"]);
44411
+ if (job)
44412
+ await run(["patch", "job", name2, "--type=merge", "-p", '{"spec":{"suspend":true}}']);
44413
+ await run([
44414
+ "delete",
44415
+ "pod",
44416
+ "-l",
44417
+ `job-name=${name2}`,
44418
+ `--grace-period=${graceSeconds}`,
44419
+ "--wait=true",
44420
+ "--ignore-not-found"
44421
+ ]);
44422
+ }
44423
+
43521
44424
  // ../drivers/src/kubernetes/kubernetes.ts
43522
44425
  class KubernetesDriver {
43523
44426
  kind = "kubernetes";
@@ -43529,7 +44432,9 @@ class KubernetesDriver {
43529
44432
  imagePullPolicy;
43530
44433
  egress;
43531
44434
  sidecarsSupported = false;
44435
+ captureEvidence;
43532
44436
  constructor(options = {}) {
44437
+ this.captureEvidence = options.captureTerminationEvidence ?? false;
43533
44438
  this.namespace = options.namespace ?? "default";
43534
44439
  if (!isKubernetesName(this.namespace)) {
43535
44440
  throw new Error("namespace must be a Kubernetes resource name");
@@ -43597,6 +44502,7 @@ class KubernetesDriver {
43597
44502
  nodeSelector: this.nodeSelector,
43598
44503
  tolerations: this.tolerations,
43599
44504
  imagePullPolicy: this.imagePullPolicy,
44505
+ ...this.captureEvidence ? { podFinalizers: [EVIDENCE_FINALIZER] } : {},
43600
44506
  egressImage: this.egress.egressImage
43601
44507
  });
43602
44508
  try {
@@ -43650,6 +44556,7 @@ class KubernetesDriver {
43650
44556
  nodeSelector: this.nodeSelector,
43651
44557
  tolerations: this.tolerations,
43652
44558
  imagePullPolicy: this.imagePullPolicy,
44559
+ ...this.captureEvidence ? { podFinalizers: [EVIDENCE_FINALIZER] } : {},
43653
44560
  egressImage: this.egress.egressImage
43654
44561
  });
43655
44562
  try {
@@ -43672,38 +44579,36 @@ class KubernetesDriver {
43672
44579
  }
43673
44580
  }
43674
44581
  async inspect(ref) {
43675
- try {
43676
- const output = await kubectl(this.kubectlBin, this.namespace, ["get", "job", ref.id, "-o", "json"]);
43677
- const job = JSON.parse(output);
43678
- const running = (job.status?.active ?? 0) > 0;
43679
- const completedExitCode = job.status?.succeeded ? 0 : 1;
43680
- return {
43681
- exists: true,
43682
- running,
43683
- exitCode: running || !(job.status?.succeeded || job.status?.failed) ? null : completedExitCode
43684
- };
43685
- } catch {
44582
+ const output = await kubectl(this.kubectlBin, this.namespace, [
44583
+ "get",
44584
+ "job",
44585
+ ref.id,
44586
+ "--ignore-not-found",
44587
+ "-o",
44588
+ "json"
44589
+ ]);
44590
+ if (!output)
43686
44591
  return { exists: false, running: false, exitCode: null };
43687
- }
44592
+ const job = JSON.parse(output);
44593
+ if (this.captureEvidence) {
44594
+ if (!job.metadata?.uid)
44595
+ throw new Error("Termination evidence unavailable");
44596
+ await retainNodeIdentities((args) => kubectl(this.kubectlBin, this.namespace, args), ref.id, job.metadata.uid);
44597
+ }
44598
+ const running = (job.status?.active ?? 0) > 0;
44599
+ const completedExitCode = job.status?.succeeded ? 0 : 1;
44600
+ return {
44601
+ exists: true,
44602
+ running,
44603
+ exitCode: running || !(job.status?.succeeded || job.status?.failed) ? null : completedExitCode
44604
+ };
43688
44605
  }
43689
44606
  async stop(ref, graceSeconds) {
43690
- await kubectl(this.kubectlBin, this.namespace, [
43691
- "patch",
43692
- "job",
43693
- ref.id,
43694
- "--type=merge",
43695
- "-p",
43696
- '{"spec":{"suspend":true}}'
43697
- ]).catch(() => {});
43698
- await kubectl(this.kubectlBin, this.namespace, [
43699
- "delete",
43700
- "pod",
43701
- "-l",
43702
- `job-name=${ref.id}`,
43703
- `--grace-period=${graceSeconds}`,
43704
- "--wait=true",
43705
- "--ignore-not-found"
43706
- ]).catch(() => {});
44607
+ if (this.captureEvidence) {
44608
+ await captureTermination((args) => kubectl(this.kubectlBin, this.namespace, args), ref.id, graceSeconds);
44609
+ return;
44610
+ }
44611
+ await stopKubernetesJob((args) => kubectl(this.kubectlBin, this.namespace, args), ref.id, graceSeconds);
43707
44612
  }
43708
44613
  async remove(ref) {
43709
44614
  await kubectl(this.kubectlBin, this.namespace, [
@@ -43711,19 +44616,31 @@ class KubernetesDriver {
43711
44616
  "job",
43712
44617
  ref.id,
43713
44618
  "--ignore-not-found",
44619
+ "--cascade=foreground",
43714
44620
  "--wait=true"
43715
- ]).catch(() => {});
44621
+ ]);
43716
44622
  const inputSecret = typeof ref.inputSecret === "string" ? ref.inputSecret : `${ref.id}-input`;
43717
- await kubectl(this.kubectlBin, this.namespace, ["delete", "secret", inputSecret, "--ignore-not-found"]).catch(() => {});
44623
+ await kubectl(this.kubectlBin, this.namespace, ["delete", "secret", inputSecret, "--ignore-not-found"]);
43718
44624
  if (typeof ref.egressSecret === "string") {
43719
- await kubectl(this.kubectlBin, this.namespace, [
43720
- "delete",
43721
- "secret",
43722
- ref.egressSecret,
43723
- "--ignore-not-found"
43724
- ]).catch(() => {});
44625
+ await kubectl(this.kubectlBin, this.namespace, ["delete", "secret", ref.egressSecret, "--ignore-not-found"]);
43725
44626
  }
43726
44627
  }
44628
+ async terminationEvidence(ref) {
44629
+ if (!this.captureEvidence)
44630
+ return null;
44631
+ return readTerminationEvidence((args) => kubectl(this.kubectlBin, this.namespace, args), ref.id);
44632
+ }
44633
+ async purgeInput(workspaceId) {
44634
+ const name2 = resourceName(workspaceId);
44635
+ await kubectl(this.kubectlBin, this.namespace, [
44636
+ "delete",
44637
+ "secret",
44638
+ `${name2}-input`,
44639
+ `${name2}-egress`,
44640
+ "--ignore-not-found",
44641
+ "--wait=true"
44642
+ ]);
44643
+ }
43727
44644
  async cleanupInput(workspaceId) {
43728
44645
  await kubectl(this.kubectlBin, this.namespace, [
43729
44646
  "delete",
@@ -44248,6 +45165,7 @@ function loadConfig(env2 = process.env) {
44248
45165
  }
44249
45166
  return {
44250
45167
  listenHost: env2.POCKETCODER_HOST ?? "127.0.0.1",
45168
+ launchPolicy: launchPolicyConfig(env2),
44251
45169
  listenPort,
44252
45170
  storeKind,
44253
45171
  databaseUrl,
@@ -44276,8 +45194,7 @@ function loadConfig(env2 = process.env) {
44276
45194
  }
44277
45195
 
44278
45196
  // ../memory-store/src/modules/auth/memory-store-auth.ts
44279
- import { randomUUID as randomUUID17 } from "crypto";
44280
-
45197
+ import { randomUUID as randomUUID19 } from "crypto";
44281
45198
  class MemoryAuthStore {
44282
45199
  context;
44283
45200
  constructor(context) {
@@ -44288,7 +45205,7 @@ class MemoryAuthStore {
44288
45205
  throw new Error(`principal exists: ${name2}`);
44289
45206
  }
44290
45207
  const row = {
44291
- id: randomUUID17(),
45208
+ id: randomUUID19(),
44292
45209
  name: name2,
44293
45210
  scopes,
44294
45211
  templateNames,
@@ -44301,6 +45218,21 @@ class MemoryAuthStore {
44301
45218
  async getPrincipalByName(name2) {
44302
45219
  return this.context.principals.find((p) => p.name === name2) ?? null;
44303
45220
  }
45221
+ async getPrincipal(id) {
45222
+ return this.context.principals.find((row) => row.id === id) ?? null;
45223
+ }
45224
+ async listMachineKeys(principalId, filter) {
45225
+ return this.context.keys.filter((row) => row.principalId === principalId && (!filter.cursor || row.id > filter.cursor) && (!filter.requestId || row.issuanceRequestId === filter.requestId)).sort((a, b) => a.id.localeCompare(b.id)).slice(0, filter.limit).map((row) => ({ ...row }));
45226
+ }
45227
+ async revokePrincipalKeys(principalId, at) {
45228
+ const principal = this.context.principals.find((row) => row.id === principalId);
45229
+ if (!principal)
45230
+ return;
45231
+ principal.disabledAt = at;
45232
+ for (const key of this.context.keys)
45233
+ if (key.principalId === principalId)
45234
+ key.revokedAt ??= at;
45235
+ }
44304
45236
  async listPrincipals() {
44305
45237
  return this.context.principals.map((p) => ({ ...p }));
44306
45238
  }
@@ -44319,7 +45251,32 @@ class MemoryAuthStore {
44319
45251
  }
44320
45252
  }
44321
45253
  async insertMachineKey(row) {
45254
+ await this.issueMachineKey({
45255
+ ...row,
45256
+ issuanceRequestId: row.issuanceRequestId ?? null,
45257
+ issuanceRequestDigest: row.issuanceRequestDigest ?? null,
45258
+ managedPrincipalIds: row.managedPrincipalIds ?? []
45259
+ });
45260
+ }
45261
+ async issueMachineKey(row) {
45262
+ const principal = this.context.principals.find((principal2) => principal2.id === row.principalId);
45263
+ if (!principal)
45264
+ throw new ApiError("auth.invalid_key", "Unknown principal.");
45265
+ const existing = row.issuanceRequestId ? this.context.keys.find((key) => key.principalId === row.principalId && key.issuanceRequestId === row.issuanceRequestId) : null;
45266
+ if (existing)
45267
+ return {
45268
+ key: { ...existing },
45269
+ created: false,
45270
+ conflict: existing.issuanceRequestDigest !== row.issuanceRequestDigest
45271
+ };
45272
+ if (principal.disabledAt)
45273
+ throw new ApiError("auth.disabled_principal", "This principal is disabled.");
45274
+ if (row.issuanceRequestId && row.expiresAt && row.expiresAt <= new Date)
45275
+ throw new ApiError("validation.invalid", "Key expiry must be in the future.");
45276
+ if (row.scopes.some((scope) => !principal.scopes.includes("admin") && !principal.scopes.includes(scope)))
45277
+ throw new ApiError("auth.missing_scope", "Key scopes exceed the principal's authority.");
44322
45278
  this.context.keys.push({ ...row });
45279
+ return { key: { ...row }, created: true, conflict: false };
44323
45280
  }
44324
45281
  async getMachineKeyWithPrincipal(keyId) {
44325
45282
  const key = this.context.keys.find((k) => k.id === keyId);
@@ -44345,7 +45302,7 @@ class MemoryAuthStore {
44345
45302
  }
44346
45303
 
44347
45304
  // ../memory-store/src/state/memory-store-base.ts
44348
- import { randomUUID as randomUUID18 } from "crypto";
45305
+ import { randomUUID as randomUUID20 } from "crypto";
44349
45306
  var ACTIVE_STATES = [
44350
45307
  "provisioning",
44351
45308
  "connected",
@@ -44419,7 +45376,7 @@ class MemoryState {
44419
45376
  }
44420
45377
  appendHistory(row, from, to, reason, at) {
44421
45378
  this.history.push({
44422
- id: randomUUID18(),
45379
+ id: randomUUID20(),
44423
45380
  workspaceId: row.id,
44424
45381
  fromState: from,
44425
45382
  toState: to,
@@ -44428,6 +45385,8 @@ class MemoryState {
44428
45385
  });
44429
45386
  }
44430
45387
  appendWorkspaceEvent(row, at) {
45388
+ if (row.purgeRequestedAt)
45389
+ return;
44431
45390
  const payload = buildEventEnvelope(row, at);
44432
45391
  this.outbox.push({
44433
45392
  id: payload.id,
@@ -44453,6 +45412,8 @@ class MemoryConversationStore {
44453
45412
  const state = this.context.conversationStates.get(input.workspaceId);
44454
45413
  if (state?.status === "deleted")
44455
45414
  throw new Error("conversation.deleted");
45415
+ if (this.context.workspaces.get(input.workspaceId)?.purgeRequestedAt)
45416
+ throw new Error("conversation.deleted");
44456
45417
  const rows = this.context.conversations.get(input.workspaceId) ?? [];
44457
45418
  const existing = rows.find((row) => row.messageId === input.messageId);
44458
45419
  if (existing)
@@ -44528,6 +45489,8 @@ class MemoryLogStore {
44528
45489
  this.context = context;
44529
45490
  }
44530
45491
  async appendLogs(workspaceId, entries) {
45492
+ if (this.context.workspaces.get(workspaceId)?.purgeRequestedAt)
45493
+ return;
44531
45494
  const list = this.context.logs.get(workspaceId) ?? [];
44532
45495
  let bytes = this.context.logBytes.get(workspaceId) ?? 0;
44533
45496
  let seq = list.length > 0 ? list[list.length - 1]?.seq ?? 0 : 0;
@@ -44564,6 +45527,8 @@ class MemoryLogStore {
44564
45527
  };
44565
45528
  }
44566
45529
  async appendNetworkEvents(workspaceId, sourceSessionId, events) {
45530
+ if (this.context.workspaces.get(workspaceId)?.purgeRequestedAt)
45531
+ return;
44567
45532
  const rows = this.context.networkEvents.get(workspaceId) ?? [];
44568
45533
  const seen = new Set(rows.map((row) => `${row.sourceSessionId}:${row.source_seq}`));
44569
45534
  const workspace = this.context.workspaces.get(workspaceId);
@@ -44585,7 +45550,7 @@ class MemoryLogStore {
44585
45550
  }
44586
45551
 
44587
45552
  // ../memory-store/src/modules/outbox/memory-store-outbox.ts
44588
- import { randomUUID as randomUUID19 } from "crypto";
45553
+ import { randomUUID as randomUUID21 } from "crypto";
44589
45554
 
44590
45555
  class MemoryOutboxStore {
44591
45556
  context;
@@ -44616,8 +45581,10 @@ class MemoryOutboxStore {
44616
45581
  this.context.claimedEvents.delete(id);
44617
45582
  }
44618
45583
  async appendEvent(workspaceId, eventType, payload, at) {
45584
+ if (this.context.workspaces.get(workspaceId)?.purgeRequestedAt)
45585
+ return;
44619
45586
  this.context.outbox.push({
44620
- id: randomUUID19(),
45587
+ id: randomUUID21(),
44621
45588
  workspaceId,
44622
45589
  eventType,
44623
45590
  payload,
@@ -44630,6 +45597,52 @@ class MemoryOutboxStore {
44630
45597
  }
44631
45598
  }
44632
45599
 
45600
+ // ../memory-store/src/modules/persistence/memory-store-content.ts
45601
+ function createContentPurge2(context) {
45602
+ return {
45603
+ async listWorkspaceStorage(workspaceId) {
45604
+ return [...context.storage.values()].filter((row) => row.workspaceId === workspaceId).map((row) => ({ ...row }));
45605
+ },
45606
+ async purgeWorkspaceContent(workspaceId, at) {
45607
+ const workspace = context.workspaces.get(workspaceId);
45608
+ if (!workspace?.purgeRequestedAt)
45609
+ throw new Error("Purge has not been admitted");
45610
+ context.logs.delete(workspaceId);
45611
+ context.logBytes.delete(workspaceId);
45612
+ context.outputs.delete(workspaceId);
45613
+ context.conversations.delete(workspaceId);
45614
+ context.conversationBytes.delete(workspaceId);
45615
+ context.networkEvents.delete(workspaceId);
45616
+ context.outbox = context.outbox.filter((row) => row.workspaceId !== workspaceId);
45617
+ context.conversationStates.set(workspaceId, {
45618
+ workspaceId,
45619
+ status: "deleted",
45620
+ expiresAt: null,
45621
+ deletedAt: at,
45622
+ updatedAt: at
45623
+ });
45624
+ for (const checkpoint of context.checkpoints.values()) {
45625
+ if (checkpoint.workspaceId === workspaceId)
45626
+ Object.assign(checkpoint, { manifest: null, manifestDigest: null, sourceProvenance: null, label: null });
45627
+ }
45628
+ Object.assign(workspace, {
45629
+ launchInput: null,
45630
+ outputs: {},
45631
+ metadata: {},
45632
+ health: {},
45633
+ failureLogTail: null,
45634
+ failureLogTailTruncated: false,
45635
+ failureLastLogSeq: null,
45636
+ sourceDescriptor: null,
45637
+ resolvedSource: null,
45638
+ registrationDigest: null,
45639
+ reconnectDigest: null,
45640
+ updatedAt: at
45641
+ });
45642
+ }
45643
+ };
45644
+ }
45645
+
44633
45646
  // ../memory-store/src/modules/persistence/memory-store-operations.ts
44634
45647
  class MemoryOperationStore {
44635
45648
  context;
@@ -44649,6 +45662,12 @@ class MemoryOperationStore {
44649
45662
  throw new OperationCapacityExceededError;
44650
45663
  }
44651
45664
  this.assertOperationReferences(row);
45665
+ const workspace = row.workspaceId ? this.context.workspaces.get(row.workspaceId) : null;
45666
+ if (workspace?.purgeRequestedAt && row.kind !== "purge") {
45667
+ throw new ApiError("operation.conflict", "Workspace content is being purged.");
45668
+ }
45669
+ if (workspace && row.kind === "purge")
45670
+ workspace.purgeRequestedAt ??= row.createdAt;
44652
45671
  this.context.operations.set(row.id, { ...row });
44653
45672
  return { operation: { ...row }, created: true, conflict: false };
44654
45673
  }
@@ -44671,12 +45690,12 @@ class MemoryOperationStore {
44671
45690
  async listIncompleteOperations() {
44672
45691
  return [...this.context.operations.values()].filter((row) => row.state === "pending" || row.state === "running").map((row) => ({ ...row }));
44673
45692
  }
44674
- async updateOperation(id, patch, at) {
45693
+ async updateOperation(id, patch2, at) {
44675
45694
  const row = this.context.operations.get(id);
44676
45695
  if (!row)
44677
45696
  return;
44678
- this.assertOperationReferences({ ...row, ...patch });
44679
- Object.assign(row, patch);
45697
+ this.assertOperationReferences({ ...row, ...patch2 });
45698
+ Object.assign(row, patch2);
44680
45699
  row.updatedAt = at;
44681
45700
  }
44682
45701
  async checkpointUsage(principalId) {
@@ -44693,6 +45712,8 @@ class MemoryOperationStore {
44693
45712
  const workspace = this.context.workspaces.get(input.workspaceId);
44694
45713
  if (!workspace)
44695
45714
  throw new Error("workspace not found");
45715
+ if (workspace.purgeRequestedAt)
45716
+ throw new ApiError("operation.conflict", "Workspace content is being purged.");
44696
45717
  const list = this.context.outputs.get(input.workspaceId) ?? [];
44697
45718
  const row = { ...input, seq: list.length + 1 };
44698
45719
  list.push(row);
@@ -44727,11 +45748,11 @@ class MemoryPersistenceStore {
44727
45748
  const row = this.context.storage.get(id);
44728
45749
  return row ? { ...row } : null;
44729
45750
  }
44730
- async updateWorkspaceStorage(id, patch, at) {
45751
+ async updateWorkspaceStorage(id, patch2, at) {
44731
45752
  const row = this.context.storage.get(id);
44732
45753
  if (!row)
44733
45754
  return;
44734
- Object.assign(row, patch);
45755
+ Object.assign(row, patch2);
44735
45756
  row.updatedAt = at;
44736
45757
  }
44737
45758
  async insertCheckpoint(row) {
@@ -44748,24 +45769,24 @@ class MemoryPersistenceStore {
44748
45769
  async listCheckpoints(principalId, filter = {}) {
44749
45770
  return [...this.context.checkpoints.values()].filter((row) => row.principalId === principalId && (!filter.workspaceId || row.workspaceId === filter.workspaceId) && (!filter.state || row.state === filter.state)).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()).map((row) => ({ ...row }));
44750
45771
  }
44751
- async updateCheckpoint(id, patch, at) {
45772
+ async updateCheckpoint(id, patch2, at) {
44752
45773
  const row = this.context.checkpoints.get(id);
44753
45774
  if (!row)
44754
45775
  return;
44755
45776
  if (row.state === "ready") {
44756
45777
  const mutable = new Set(["state", "reasonCode", "expiresAt", "deletedAt"]);
44757
- for (const key of Object.keys(patch)) {
45778
+ for (const key of Object.keys(patch2)) {
44758
45779
  if (!mutable.has(key))
44759
45780
  throw new Error("ready checkpoints are immutable");
44760
45781
  }
44761
45782
  }
44762
- Object.assign(row, patch);
45783
+ Object.assign(row, patch2);
44763
45784
  row.updatedAt = at;
44764
45785
  }
44765
45786
  }
44766
45787
 
44767
45788
  // ../memory-store/src/modules/templates/memory-store-templates.ts
44768
- import { randomUUID as randomUUID20 } from "crypto";
45789
+ import { randomUUID as randomUUID22 } from "crypto";
44769
45790
 
44770
45791
  class MemoryTemplateStore {
44771
45792
  context;
@@ -44781,7 +45802,7 @@ class MemoryTemplateStore {
44781
45802
  return { row: existing, created: false, conflict: false };
44782
45803
  }
44783
45804
  const row = {
44784
- id: randomUUID20(),
45805
+ id: randomUUID22(),
44785
45806
  name: input.name,
44786
45807
  version: input.version,
44787
45808
  digest: input.digest,
@@ -44887,16 +45908,16 @@ class MemoryWarmPoolStore {
44887
45908
  async listWarmPoolRuntimes() {
44888
45909
  return [...this.context.warmPoolRuntimes.values()].map((row) => ({ ...row }));
44889
45910
  }
44890
- async updateWarmPoolRuntime(id, patch, at) {
45911
+ async updateWarmPoolRuntime(id, patch2, at) {
44891
45912
  const row = this.context.warmPoolRuntimes.get(id);
44892
45913
  if (!row)
44893
45914
  return;
44894
- Object.assign(row, patch);
45915
+ Object.assign(row, patch2);
44895
45916
  row.updatedAt = at;
44896
45917
  }
44897
45918
  async claimWarmPoolRuntime(claim) {
44898
45919
  const workspace = this.context.workspaces.get(claim.workspaceId);
44899
- if (workspace?.state !== "queued")
45920
+ if (workspace?.state !== "queued" || workspace.purgeRequestedAt)
44900
45921
  return null;
44901
45922
  const runtime = [...this.context.warmPoolRuntimes.values()].filter((row) => row.state === "ready" && row.templateDigest === claim.templateDigest && row.driverKind === claim.driverKind && row.eligibilityFingerprint === claim.eligibilityFingerprint).sort((a, b) => (a.readyAt?.getTime() ?? 0) - (b.readyAt?.getTime() ?? 0))[0];
44902
45923
  if (!runtime?.providerRef)
@@ -44958,7 +45979,7 @@ class MemoryAdmissionStore {
44958
45979
  }
44959
45980
  async claimWorkspaceAdmission(claim) {
44960
45981
  const workspace = this.context.workspaces.get(claim.workspaceId);
44961
- if (workspace?.state !== "queued")
45982
+ if (workspace?.state !== "queued" || workspace.purgeRequestedAt)
44962
45983
  return null;
44963
45984
  const counts = this.activeCounts();
44964
45985
  if (counts.global >= claim.limits.globalActiveWorkspaces)
@@ -44981,12 +46002,14 @@ class MemoryAdmissionStore {
44981
46002
  }
44982
46003
  });
44983
46004
  }
44984
- async updateWorkspace(id, patch, at) {
46005
+ async updateWorkspace(id, patch2, at) {
44985
46006
  const row = this.context.workspaces.get(id);
44986
46007
  if (!row)
44987
46008
  return;
44988
- Object.assign(row, patch);
44989
- const changed = Object.keys(patch).some((key) => CHANGE_PATCH_KEYS2.has(key));
46009
+ Object.assign(row, patch2);
46010
+ if (row.purgeRequestedAt)
46011
+ Object.assign(row, purgedContentPatch());
46012
+ const changed = Object.keys(patch2).some((key) => CHANGE_PATCH_KEYS2.has(key));
44990
46013
  if (changed)
44991
46014
  row.changeSeq += 1;
44992
46015
  row.updatedAt = at;
@@ -45035,12 +46058,16 @@ class MemoryAdmissionStore {
45035
46058
  return null;
45036
46059
  if (!canTransition(row.state, req.to))
45037
46060
  return null;
46061
+ if (row.purgeRequestedAt && ["provisioning", "connected", "ready", "preserving", "queued"].includes(req.to))
46062
+ return null;
45038
46063
  const fromState = row.state;
45039
46064
  row.state = req.to;
45040
46065
  if (req.reason !== undefined)
45041
46066
  row.reasonCode = req.reason;
45042
46067
  if (req.patch)
45043
46068
  Object.assign(row, req.patch);
46069
+ if (row.purgeRequestedAt)
46070
+ Object.assign(row, purgedContentPatch());
45044
46071
  row.changeSeq += 1;
45045
46072
  row.updatedAt = req.at;
45046
46073
  if (isTerminal(req.to)) {
@@ -45137,6 +46164,7 @@ class MemoryWorkspaceStore {
45137
46164
  createdAt: row.createdAt,
45138
46165
  updatedAt: row.createdAt,
45139
46166
  terminalAt: null,
46167
+ purgeRequestedAt: null,
45140
46168
  originWorkspaceId: row.originWorkspaceId ?? null,
45141
46169
  restoredFromCheckpointId: row.restoredFromCheckpointId ?? null,
45142
46170
  sourceDescriptor: row.sourceDescriptor ?? null,
@@ -45189,6 +46217,9 @@ class MemoryWorkspaceStore {
45189
46217
  class MemoryStore {
45190
46218
  constructor() {
45191
46219
  this.context = new MemoryState;
46220
+ const content = createContentPurge2(this.context);
46221
+ this.listWorkspaceStorage = content.listWorkspaceStorage;
46222
+ this.purgeWorkspaceContent = content.purgeWorkspaceContent;
45192
46223
  this.persistence = new MemoryPersistenceStore(this.context);
45193
46224
  this.operation = new MemoryOperationStore(this.context);
45194
46225
  this.template = new MemoryTemplateStore(this.context);
@@ -45233,6 +46264,10 @@ class MemoryStore {
45233
46264
  this.listPrincipals = this.auth.listPrincipals.bind(this.auth);
45234
46265
  this.updatePrincipal = this.auth.updatePrincipal.bind(this.auth);
45235
46266
  this.setPrincipalDisabled = this.auth.setPrincipalDisabled.bind(this.auth);
46267
+ this.getPrincipal = this.auth.getPrincipal.bind(this.auth);
46268
+ this.issueMachineKey = this.auth.issueMachineKey.bind(this.auth);
46269
+ this.listMachineKeys = this.auth.listMachineKeys.bind(this.auth);
46270
+ this.revokePrincipalKeys = this.auth.revokePrincipalKeys.bind(this.auth);
45236
46271
  this.insertMachineKey = this.auth.insertMachineKey.bind(this.auth);
45237
46272
  this.getMachineKeyWithPrincipal = this.auth.getMachineKeyWithPrincipal.bind(this.auth);
45238
46273
  this.revokeMachineKey = this.auth.revokeMachineKey.bind(this.auth);
@@ -45283,6 +46318,8 @@ class MemoryStore {
45283
46318
  operation;
45284
46319
  persistence;
45285
46320
  context;
46321
+ listWorkspaceStorage;
46322
+ purgeWorkspaceContent;
45286
46323
  init;
45287
46324
  acquireCoordinatorLease;
45288
46325
  close;
@@ -45316,6 +46353,10 @@ class MemoryStore {
45316
46353
  listPrincipals;
45317
46354
  updatePrincipal;
45318
46355
  setPrincipalDisabled;
46356
+ getPrincipal;
46357
+ issueMachineKey;
46358
+ listMachineKeys;
46359
+ revokePrincipalKeys;
45319
46360
  insertMachineKey;
45320
46361
  getMachineKeyWithPrincipal;
45321
46362
  revokeMachineKey;
@@ -45563,10 +46604,10 @@ function mapValues(object2, mapper) {
45563
46604
  });
45564
46605
  return result;
45565
46606
  }
45566
- function omit2(object2, keys) {
46607
+ function omit2(object2, keys2) {
45567
46608
  const result = {};
45568
46609
  Object.entries(object2).forEach(([key, value]) => {
45569
- if (!keys.some((keyToOmit) => keyToOmit === key)) {
46610
+ if (!keys2.some((keyToOmit) => keyToOmit === key)) {
45570
46611
  result[key] = value;
45571
46612
  }
45572
46613
  });
@@ -45941,8 +46982,8 @@ class DiscriminatedUnionTransformer {
45941
46982
  const refId = Metadata.getRefId(obj);
45942
46983
  const value = (_a4 = obj.def.shape) === null || _a4 === undefined ? undefined : _a4[discriminator];
45943
46984
  if (isZodType(value, "ZodEnum")) {
45944
- const keys = Object.values(value._zod.def.entries).filter(isString);
45945
- keys.forEach((enumValue) => {
46985
+ const keys2 = Object.values(value._zod.def.entries).filter(isString);
46986
+ keys2.forEach((enumValue) => {
45946
46987
  mapping[enumValue] = generateSchemaRef(refId);
45947
46988
  });
45948
46989
  return;
@@ -46069,8 +47110,8 @@ class RecordTransformer {
46069
47110
  const keyType = zodSchema.keyType;
46070
47111
  const propertiesSchema = isAnyZodType(propertiesType) ? mapItem(propertiesType) : {};
46071
47112
  if (isZodType(keyType, "ZodEnum")) {
46072
- const keys = Object.values(keyType._zod.def.entries).filter(isString);
46073
- const properties = keys.reduce((acc, curr) => Object.assign(Object.assign({}, acc), { [curr]: propertiesSchema }), {});
47113
+ const keys2 = Object.values(keyType._zod.def.entries).filter(isString);
47114
+ const properties = keys2.reduce((acc, curr) => Object.assign(Object.assign({}, acc), { [curr]: propertiesSchema }), {});
46074
47115
  return Object.assign(Object.assign({}, mapNullableType("object")), { properties });
46075
47116
  }
46076
47117
  return Object.assign(Object.assign({}, mapNullableType("object")), { additionalProperties: propertiesSchema });
@@ -47372,9 +48413,9 @@ var handleParsingNestedValues = (form, key, value) => {
47372
48413
  return;
47373
48414
  }
47374
48415
  let nestedForm = form;
47375
- const keys = key.split(".");
47376
- keys.forEach((key2, index2) => {
47377
- if (index2 === keys.length - 1) {
48416
+ const keys2 = key.split(".");
48417
+ keys2.forEach((key2, index2) => {
48418
+ if (index2 === keys2.length - 1) {
47378
48419
  nestedForm[key2] = value;
47379
48420
  } else {
47380
48421
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
@@ -47410,8 +48451,8 @@ var HonoRequest = class {
47410
48451
  }
47411
48452
  #getAllDecodedParams() {
47412
48453
  const decoded = {};
47413
- const keys = Object.keys(this.#matchResult[0][this.routeIndex][1]);
47414
- for (const key of keys) {
48454
+ const keys2 = Object.keys(this.#matchResult[0][this.routeIndex][1]);
48455
+ for (const key of keys2) {
47415
48456
  const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]);
47416
48457
  if (value !== undefined) {
47417
48458
  decoded[key] = /\%/.test(value) ? tryDecodeURIComponent(value) : value;
@@ -48170,9 +49211,9 @@ function buildMatcherFromPreprocessedRoutes(routes) {
48170
49211
  if (!map2) {
48171
49212
  continue;
48172
49213
  }
48173
- const keys = Object.keys(map2);
48174
- for (let k = 0, len3 = keys.length;k < len3; k++) {
48175
- map2[keys[k]] = paramReplacementMap[map2[keys[k]]];
49214
+ const keys2 = Object.keys(map2);
49215
+ for (let k = 0, len3 = keys2.length;k < len3; k++) {
49216
+ map2[keys2[k]] = paramReplacementMap[map2[keys2[k]]];
48176
49217
  }
48177
49218
  }
48178
49219
  }
@@ -48975,10 +50016,10 @@ var createBunWebSocket = () => ({
48975
50016
  });
48976
50017
 
48977
50018
  // ../server/src/http/middleware.ts
48978
- import { randomUUID as randomUUID21 } from "crypto";
50019
+ import { randomUUID as randomUUID23 } from "crypto";
48979
50020
  var requestId = async (c, next) => {
48980
50021
  const candidate = c.req.header("x-request-id");
48981
- const id = candidate && /^[A-Za-z0-9._-]{1,128}$/.test(candidate) ? candidate : randomUUID21();
50022
+ const id = candidate && /^[A-Za-z0-9._-]{1,128}$/.test(candidate) ? candidate : randomUUID23();
48982
50023
  c.set("requestId", id);
48983
50024
  c.header("x-request-id", id);
48984
50025
  await next();
@@ -49025,6 +50066,7 @@ function machineAuth(store, pepper) {
49025
50066
  c.set("principal", found.principal);
49026
50067
  c.set("scopes", effectiveScopes);
49027
50068
  c.set("keyId", found.key.id);
50069
+ c.set("managedPrincipalIds", found.key.managedPrincipalIds);
49028
50070
  store.touchMachineKey(found.key.id, now).catch(() => {});
49029
50071
  await next();
49030
50072
  };
@@ -49039,7 +50081,7 @@ function requireScope(scope) {
49039
50081
  }
49040
50082
  function errorHandler2(logger) {
49041
50083
  return (err, c) => {
49042
- const id = c.get("requestId") ?? randomUUID21();
50084
+ const id = c.get("requestId") ?? randomUUID23();
49043
50085
  if (err instanceof ApiError) {
49044
50086
  return c.json(errorEnvelope(err.code, err.message, id, err.details), err.status);
49045
50087
  }
@@ -49141,8 +50183,219 @@ function registerAdministrationRoutes({
49141
50183
  });
49142
50184
  }
49143
50185
 
50186
+ // ../../node_modules/.bun/hono@4.12.32/node_modules/hono/dist/middleware/body-limit/index.js
50187
+ var ERROR_MESSAGE = "Payload Too Large";
50188
+ var bodyLimit = (options) => {
50189
+ const onError = options.onError || (() => {
50190
+ const res = new Response(ERROR_MESSAGE, {
50191
+ status: 413
50192
+ });
50193
+ throw new HTTPException(413, { res });
50194
+ });
50195
+ const maxSize = options.maxSize;
50196
+ return async function bodyLimit2(c, next) {
50197
+ if (!c.req.raw.body) {
50198
+ return next();
50199
+ }
50200
+ const hasTransferEncoding = c.req.raw.headers.has("transfer-encoding");
50201
+ const hasContentLength = c.req.raw.headers.has("content-length");
50202
+ if (hasContentLength && !hasTransferEncoding) {
50203
+ const contentLength = parseInt(c.req.raw.headers.get("content-length") || "0", 10);
50204
+ return contentLength > maxSize ? onError(c) : next();
50205
+ }
50206
+ let size = 0;
50207
+ const chunks = [];
50208
+ const rawReader = c.req.raw.body.getReader();
50209
+ for (;; ) {
50210
+ const { done, value } = await rawReader.read();
50211
+ if (done) {
50212
+ break;
50213
+ }
50214
+ size += value.length;
50215
+ if (size > maxSize) {
50216
+ return onError(c);
50217
+ }
50218
+ chunks.push(value);
50219
+ }
50220
+ const requestInit = {
50221
+ body: new ReadableStream({
50222
+ start(controller) {
50223
+ for (const chunk of chunks) {
50224
+ controller.enqueue(chunk);
50225
+ }
50226
+ controller.close();
50227
+ }
50228
+ }),
50229
+ duplex: "half"
50230
+ };
50231
+ c.req.raw = new Request(c.req.raw, requestInit);
50232
+ return next();
50233
+ };
50234
+ };
50235
+
50236
+ // ../server/src/administration/principal-authority.ts
50237
+ async function managedPrincipal(context, store, id) {
50238
+ if (!context.get("managedPrincipalIds").includes(id))
50239
+ throw new ApiError("principal.not_found", "Unknown principal.");
50240
+ const principal = await store.getPrincipal(id);
50241
+ if (!principal)
50242
+ throw new ApiError("principal.not_found", "Unknown principal.");
50243
+ return principal;
50244
+ }
50245
+
50246
+ // ../server/src/administration/keys-routes.ts
50247
+ var principalParams = exports_external.object({ principalId: exports_external.uuid() });
50248
+ var revokedSchema = exports_external.object({ revoked: exports_external.literal(true) });
50249
+ function registerKeyRoutes(app, store, pepper) {
50250
+ app.use("/v1/principals/*", bodyLimit({
50251
+ maxSize: 16384,
50252
+ onError: () => {
50253
+ throw new ApiError("validation.invalid", "Request body exceeds 16384 bytes.");
50254
+ }
50255
+ }));
50256
+ app.openapi(createRoute({
50257
+ method: "get",
50258
+ path: "/v1/principals/{principalId}/keys",
50259
+ operationId: "listPrincipalKeys",
50260
+ tags: ["Keys"],
50261
+ middleware: [requireScope("keys:read")],
50262
+ request: {
50263
+ params: principalParams,
50264
+ query: exports_external.object({
50265
+ limit: exports_external.coerce.number().int().min(1).max(100).default(50),
50266
+ cursor: exports_external.uuid().optional(),
50267
+ request_id: exports_external.string().min(1).max(128).optional()
50268
+ })
50269
+ },
50270
+ responses: {
50271
+ 200: {
50272
+ description: "Authoritative key metadata",
50273
+ content: { "application/json": { schema: KeyListResponseSchema } }
50274
+ },
50275
+ ...COMMON_ERROR_RESPONSES
50276
+ }
50277
+ }), async (c) => {
50278
+ const principal = await managedPrincipal(c, store, c.req.valid("param").principalId);
50279
+ const query = c.req.valid("query");
50280
+ const keys2 = await store.listMachineKeys(principal.id, {
50281
+ limit: query.limit + 1,
50282
+ ...query.cursor ? { cursor: query.cursor } : {},
50283
+ ...query.request_id ? { requestId: query.request_id } : {}
50284
+ });
50285
+ const items = keys2.slice(0, query.limit).map((key) => keyResource(key, principal));
50286
+ return c.json({ items, next_cursor: keys2.length > query.limit ? items.at(-1)?.id ?? null : null }, 200);
50287
+ });
50288
+ app.openapi(createRoute({
50289
+ method: "post",
50290
+ path: "/v1/principals/{principalId}/keys",
50291
+ operationId: "issuePrincipalKey",
50292
+ tags: ["Keys"],
50293
+ middleware: [requireScope("keys:write")],
50294
+ request: {
50295
+ params: principalParams,
50296
+ body: { content: { "application/json": { schema: KeyIssueRequestSchema } } }
50297
+ },
50298
+ responses: {
50299
+ 200: {
50300
+ description: "Reconciled issuance; secret unavailable",
50301
+ content: { "application/json": { schema: KeyIssueResponseSchema } }
50302
+ },
50303
+ 201: {
50304
+ description: "Issued key; secret shown once",
50305
+ content: { "application/json": { schema: KeyIssueResponseSchema } }
50306
+ },
50307
+ ...COMMON_ERROR_RESPONSES
50308
+ }
50309
+ }), async (c) => {
50310
+ const principal = await managedPrincipal(c, store, c.req.valid("param").principalId);
50311
+ const result = await issuePrincipalKey(store, pepper, principal, c.req.valid("json"));
50312
+ return c.json({ key: result.key, token: result.token }, result.created ? 201 : 200);
50313
+ });
50314
+ app.openapi(createRoute({
50315
+ method: "delete",
50316
+ path: "/v1/principals/{principalId}/keys/{keyId}",
50317
+ operationId: "revokePrincipalKey",
50318
+ tags: ["Keys"],
50319
+ middleware: [requireScope("keys:write")],
50320
+ request: { params: principalParams.extend({ keyId: exports_external.uuid() }) },
50321
+ responses: {
50322
+ 200: { description: "Key revoked", content: { "application/json": { schema: revokedSchema } } },
50323
+ ...COMMON_ERROR_RESPONSES
50324
+ }
50325
+ }), async (c) => {
50326
+ const { principalId, keyId } = c.req.valid("param");
50327
+ await managedPrincipal(c, store, principalId);
50328
+ const found = await store.getMachineKeyWithPrincipal(keyId);
50329
+ if (!found || found.key.principalId !== principalId)
50330
+ throw new ApiError("key.not_found", "Unknown key.");
50331
+ await store.revokeMachineKey(keyId, new Date);
50332
+ return c.json({ revoked: true }, 200);
50333
+ });
50334
+ app.openapi(createRoute({
50335
+ method: "delete",
50336
+ path: "/v1/principals/{principalId}/keys",
50337
+ operationId: "revokeAllPrincipalKeys",
50338
+ tags: ["Keys"],
50339
+ middleware: [requireScope("keys:write")],
50340
+ request: { params: principalParams },
50341
+ responses: {
50342
+ 200: {
50343
+ description: "Principal disabled and all keys revoked atomically",
50344
+ content: { "application/json": { schema: revokedSchema } }
50345
+ },
50346
+ ...COMMON_ERROR_RESPONSES
50347
+ }
50348
+ }), async (c) => {
50349
+ const principal = await managedPrincipal(c, store, c.req.valid("param").principalId);
50350
+ await store.revokePrincipalKeys(principal.id, new Date);
50351
+ return c.json({ revoked: true }, 200);
50352
+ });
50353
+ }
50354
+
50355
+ // ../server/src/administration/recovery-routes.ts
50356
+ function registerOperatorRecoveryRoutes(app, store, persistence2) {
50357
+ const responses = {
50358
+ 202: {
50359
+ description: "Recovery purge accepted",
50360
+ content: { "application/json": { schema: OperationResourceSchema } }
50361
+ },
50362
+ ...COMMON_ERROR_RESPONSES
50363
+ };
50364
+ app.openapi(createRoute({
50365
+ method: "post",
50366
+ path: "/v1/principals/{principalId}/workspaces/{id}/purge",
50367
+ operationId: "recoverWorkspacePurge",
50368
+ tags: ["Recovery"],
50369
+ middleware: [requireScope("workspaces:recover")],
50370
+ request: {
50371
+ params: exports_external.object({ principalId: exports_external.uuid(), id: exports_external.uuid() }),
50372
+ headers: IdempotencyHeadersSchema,
50373
+ body: { content: { "application/json": { schema: exports_external.strictObject({}) } } }
50374
+ },
50375
+ responses
50376
+ }), async (c) => {
50377
+ const principal = await managedPrincipal(c, store, c.req.valid("param").principalId);
50378
+ return c.json(toOperationResource(await persistence2.purge(principal, c.req.valid("param").id, c.req.valid("header")["Idempotency-Key"])), 202);
50379
+ });
50380
+ app.openapi(createRoute({
50381
+ method: "get",
50382
+ path: "/v1/principals/{principalId}/operations/{id}",
50383
+ operationId: "getRecoveryOperation",
50384
+ tags: ["Recovery"],
50385
+ middleware: [requireScope("workspaces:recover")],
50386
+ request: { params: exports_external.object({ principalId: exports_external.uuid(), id: exports_external.uuid() }) },
50387
+ responses: { 200: responses[202], ...COMMON_ERROR_RESPONSES }
50388
+ }), async (c) => {
50389
+ const principal = await managedPrincipal(c, store, c.req.valid("param").principalId);
50390
+ const operation = await store.getOperation(c.req.valid("param").id);
50391
+ if (!operation || operation.principalId !== principal.id || operation.kind !== "purge")
50392
+ throw new ApiError("workspace.not_found", "Unknown operation.");
50393
+ return c.json(toOperationResource(operation), 200);
50394
+ });
50395
+ }
50396
+
49144
50397
  // ../server/src/attachments/attachments.ts
49145
- import { randomUUID as randomUUID22 } from "crypto";
50398
+ import { randomUUID as randomUUID24 } from "crypto";
49146
50399
  var ACK_TIMEOUT_MS = 30000;
49147
50400
  var RESULT_TIMEOUT_MS = 30000;
49148
50401
  var RESOLVE_TIMEOUT_MS = 1e4;
@@ -49283,6 +50536,8 @@ function attachmentUploadHandler(deps) {
49283
50536
  const id = c.req.param("id") ?? "";
49284
50537
  const attachmentId = (c.req.param("attachmentId") ?? "").toLowerCase();
49285
50538
  const row = await deps.service.getOwned(principal, id);
50539
+ if (row.purgeRequestedAt)
50540
+ throw new ApiError("operation.conflict", "Workspace content is being purged.");
49286
50541
  if (isTerminal(row.state)) {
49287
50542
  throw new ApiError("workspace.terminal", "This workspace has ended.");
49288
50543
  }
@@ -49294,7 +50549,7 @@ function attachmentUploadHandler(deps) {
49294
50549
  }
49295
50550
  const headers = uploadHeaders(c);
49296
50551
  const conn = liveConnection(deps.hub, id);
49297
- const operationId = randomUUID22();
50552
+ const operationId = randomUUID24();
49298
50553
  deps.hub.openAttachment(conn, operationId);
49299
50554
  try {
49300
50555
  const result = await streamUpload(deps.hub, conn, operationId, c.req.raw.body, headers, attachmentId);
@@ -49319,7 +50574,7 @@ function abandonUpload(hub, workspaceId, conn, operationId, error51) {
49319
50574
  hub.send(conn, "attachment_abort", { operation_id: operationId, reason });
49320
50575
  }
49321
50576
  async function resolveDescriptors(hub, conn, attachmentIds) {
49322
- const operationId = randomUUID22();
50577
+ const operationId = randomUUID24();
49323
50578
  hub.openAttachment(conn, operationId);
49324
50579
  try {
49325
50580
  hub.send(conn, "attachment_resolve", {
@@ -49373,7 +50628,7 @@ ${attachmentManifest(descriptors)}`;
49373
50628
  }
49374
50629
 
49375
50630
  // ../server/src/control-channel/hub.ts
49376
- import { randomUUID as randomUUID23 } from "crypto";
50631
+ import { randomUUID as randomUUID25 } from "crypto";
49377
50632
 
49378
50633
  // ../server/src/relay/relay-stream-channel.ts
49379
50634
  class RelayStreamRegistry {
@@ -49857,12 +51112,12 @@ class Hub {
49857
51112
  relay(workspaceId, request) {
49858
51113
  const conn = this.byWorkspace.get(workspaceId);
49859
51114
  if (!conn?.registered) {
49860
- return Promise.resolve({ request_id: randomUUID23(), headers: {}, error_code: "unreachable" });
51115
+ return Promise.resolve({ request_id: randomUUID25(), headers: {}, error_code: "unreachable" });
49861
51116
  }
49862
51117
  if (conn.inflight.size + conn.streams.size >= MAX_INFLIGHT_RELAY) {
49863
- return Promise.resolve({ request_id: randomUUID23(), headers: {}, error_code: "deadline" });
51118
+ return Promise.resolve({ request_id: randomUUID25(), headers: {}, error_code: "deadline" });
49864
51119
  }
49865
- const requestId2 = randomUUID23();
51120
+ const requestId2 = randomUUID25();
49866
51121
  return new Promise((resolve11) => {
49867
51122
  const timer = setTimeout(() => {
49868
51123
  conn.inflight.delete(requestId2);
@@ -49874,7 +51129,7 @@ class Hub {
49874
51129
  }
49875
51130
  relayStream(workspaceId, request, maxResponseBytes) {
49876
51131
  const conn = this.byWorkspace.get(workspaceId);
49877
- const requestId2 = randomUUID23();
51132
+ const requestId2 = randomUUID25();
49878
51133
  if (!conn?.registered) {
49879
51134
  return Promise.resolve({ request_id: requestId2, headers: {}, error_code: "unreachable" });
49880
51135
  }
@@ -50088,7 +51343,7 @@ async function authenticateConnection(deps, credentials) {
50088
51343
  return { error: "Missing workspace credentials." };
50089
51344
  }
50090
51345
  const row = await deps.store.getWorkspace(credentials.workspaceId);
50091
- if (!row || isTerminal(row.state))
51346
+ if (!row || row.purgeRequestedAt || isTerminal(row.state))
50092
51347
  return { error: "Unknown workspace." };
50093
51348
  if (sourceCredentialReference2(row) && credentials.protocolVersion < SOURCE_CREDENTIAL_MIN_PROTOCOL_VERSION) {
50094
51349
  return { error: "Source credentials require agent protocol version 6." };
@@ -50534,7 +51789,7 @@ function agentWsEvents(deps) {
50534
51789
  };
50535
51790
  }
50536
51791
  // ../server/src/conversations/conversations-routes.ts
50537
- import { randomUUID as randomUUID24 } from "crypto";
51792
+ import { randomUUID as randomUUID26 } from "crypto";
50538
51793
 
50539
51794
  // ../server/src/http/pagination.ts
50540
51795
  function encodeCursor(scope, value) {
@@ -50648,7 +51903,7 @@ function registerConversationRoutes({
50648
51903
  await store.deleteConversation(workspace.id, at);
50649
51904
  if (previous?.status !== "deleted") {
50650
51905
  await store.appendEvent(workspace.id, "workspace.conversation_deleted", {
50651
- id: randomUUID24(),
51906
+ id: randomUUID26(),
50652
51907
  type: "workspace.conversation_deleted",
50653
51908
  occurred_at: at.toISOString(),
50654
51909
  workspace_id: workspace.id
@@ -50846,7 +52101,9 @@ var HEALTHY_CHECKS = {
50846
52101
  database: "ok",
50847
52102
  schema: "ok",
50848
52103
  reconciliation: "ok",
50849
- coordinator: "ok"
52104
+ coordinator: "ok",
52105
+ "policy-reconciliation": "disabled",
52106
+ cleanup: "ok"
50850
52107
  };
50851
52108
 
50852
52109
  class Readiness {
@@ -51090,6 +52347,40 @@ function registerCheckpointRoutes(deps) {
51090
52347
  });
51091
52348
  }
51092
52349
 
52350
+ // ../server/src/persistence/purge-routes.ts
52351
+ function registerPurgeRoutes(app, persistence2) {
52352
+ app.openapi(createRoute({
52353
+ method: "post",
52354
+ path: "/v1/workspaces/{id}/purge",
52355
+ operationId: "purgeWorkspace",
52356
+ tags: ["Workspaces"],
52357
+ middleware: [
52358
+ requireScope("workspaces:purge"),
52359
+ bodyLimit({
52360
+ maxSize: 1024,
52361
+ onError: () => {
52362
+ throw new ApiError("validation.invalid", "Purge requires an empty JSON object.");
52363
+ }
52364
+ })
52365
+ ],
52366
+ request: {
52367
+ params: exports_external.object({ id: exports_external.uuid() }),
52368
+ headers: IdempotencyHeadersSchema,
52369
+ body: { content: { "application/json": { schema: exports_external.strictObject({}) } } }
52370
+ },
52371
+ responses: {
52372
+ 202: {
52373
+ description: "Durable purge accepted",
52374
+ content: { "application/json": { schema: OperationResourceSchema } }
52375
+ },
52376
+ ...COMMON_ERROR_RESPONSES
52377
+ }
52378
+ }), async (c) => {
52379
+ const operation = await persistence2.purge(c.get("principal"), c.req.valid("param").id, c.req.valid("header")["Idempotency-Key"]);
52380
+ return c.json(toOperationResource(operation), 202);
52381
+ });
52382
+ }
52383
+
51093
52384
  // ../server/src/persistence/recovery-routes.ts
51094
52385
  function registerRecoveryRoutes({
51095
52386
  app,
@@ -51201,6 +52492,14 @@ function registerRecoveryRoutes({
51201
52492
 
51202
52493
  // ../server/src/relay/relay.ts
51203
52494
  var SAFE_RESPONSE_HEADERS = ["content-type", "cache-control"];
52495
+ function assertRelayReady(row) {
52496
+ if (row.purgeRequestedAt)
52497
+ throw new ApiError("operation.conflict", "Workspace content is being purged.");
52498
+ if (isTerminal(row.state))
52499
+ throw new ApiError("workspace.terminal", "This workspace has ended.");
52500
+ if (row.state !== "ready")
52501
+ throw new ApiError("workspace.not_ready", "Workspace is not ready.");
52502
+ }
51204
52503
  function requestPath(c, prefix) {
51205
52504
  const rawPath = c.req.path.startsWith(prefix) ? c.req.path.slice(prefix.length) : "";
51206
52505
  return rawPath === "" ? "/" : rawPath;
@@ -51299,12 +52598,7 @@ function relayHandler(deps, options) {
51299
52598
  const id = c.req.param("id") ?? "";
51300
52599
  const serviceName = options?.service ?? c.req.param("service") ?? "";
51301
52600
  const row = await deps.service.getOwned(principal, id);
51302
- if (isTerminal(row.state)) {
51303
- throw new ApiError("workspace.terminal", "This workspace has ended.");
51304
- }
51305
- if (row.state !== "ready") {
51306
- throw new ApiError("workspace.not_ready", "Workspace is not ready.");
51307
- }
52601
+ assertRelayReady(row);
51308
52602
  const prefix = options?.pathPrefix?.(id) ?? `/v1/workspaces/${id}/services/${serviceName}`;
51309
52603
  const path = requestPath(c, prefix);
51310
52604
  const method = c.req.method;
@@ -51316,6 +52610,8 @@ function relayHandler(deps, options) {
51316
52610
  let bodyB64 = await requestBody(c, method, match2.route.maxRequestBytes);
51317
52611
  if (options?.transformBodyB64)
51318
52612
  bodyB64 = await options.transformBodyB64(c, bodyB64);
52613
+ if ((await deps.store.getWorkspace(id))?.purgeRequestedAt)
52614
+ throw new ApiError("operation.conflict", "Workspace content is being purged.");
51319
52615
  if (!deps.hub.isConnected(id)) {
51320
52616
  throw new ApiError("workspace.disconnected", "The workspace supervisor is not currently connected.");
51321
52617
  }
@@ -51425,7 +52721,7 @@ function registerCatalogRoutes({ app, store }) {
51425
52721
  }
51426
52722
 
51427
52723
  // ../server/src/terminals/terminal-ws.ts
51428
- import { randomUUID as randomUUID25 } from "crypto";
52724
+ import { randomUUID as randomUUID27 } from "crypto";
51429
52725
  var SessionIdSchema = exports_external.uuid();
51430
52726
  function terminalConnectValidator(deps) {
51431
52727
  return async (c, next) => {
@@ -51438,6 +52734,8 @@ function terminalConnectValidator(deps) {
51438
52734
  async function terminalCapability(deps, c) {
51439
52735
  const workspaceId = c.req.param("id") ?? "";
51440
52736
  const workspace = await deps.service.getOwned(c.get("principal"), workspaceId);
52737
+ if (workspace.purgeRequestedAt)
52738
+ throw new ApiError("operation.conflict", "Workspace content is being purged.");
51441
52739
  if (isTerminal(workspace.state)) {
51442
52740
  throw new ApiError("workspace.terminal", "This workspace has ended.");
51443
52741
  }
@@ -51466,7 +52764,7 @@ async function terminalSession(deps, c, workspaceId, maxSessions) {
51466
52764
  if (c.req.header("upgrade")?.toLowerCase() !== "websocket") {
51467
52765
  throw new ApiError("validation.invalid", "WebSocket upgrade required.");
51468
52766
  }
51469
- const sessionId = parsedSessionId?.data ?? randomUUID25();
52767
+ const sessionId = parsedSessionId?.data ?? randomUUID27();
51470
52768
  if (requested) {
51471
52769
  const session2 = await deps.store.getTerminalSession(sessionId);
51472
52770
  if (!session2 || session2.workspaceId !== workspaceId) {
@@ -51769,6 +53067,7 @@ function buildServer(deps) {
51769
53067
  }) : undefined;
51770
53068
  const persistenceHolder = {};
51771
53069
  const scheduler = new Scheduler({
53070
+ ...deps.authorizeLaunch ? { authorizeLaunch: deps.authorizeLaunch } : {},
51772
53071
  store,
51773
53072
  driver,
51774
53073
  ...deps.storageDriver ? { storageDriver: deps.storageDriver } : {},
@@ -51869,9 +53168,12 @@ function buildServer(deps) {
51869
53168
  });
51870
53169
  app.use("/v1/*", machineAuth(store, pepper));
51871
53170
  registerCatalogRoutes({ app, store });
53171
+ registerKeyRoutes(app, store, pepper);
53172
+ registerOperatorRecoveryRoutes(app, store, persistence2);
51872
53173
  registerAdministrationRoutes({ app, persistence: persistence2, warmPool });
51873
53174
  registerCheckpointRoutes({ app, store, service, persistence: persistence2 });
51874
53175
  registerRecoveryRoutes({ app, store, service, persistence: persistence2 });
53176
+ registerPurgeRoutes(app, persistence2);
51875
53177
  registerConversationRoutes({ app, store, service });
51876
53178
  registerWorkspaceRoutes({ app, store, service });
51877
53179
  registerDiagnosticRoutes({ app, store, service });
@@ -51907,6 +53209,98 @@ function buildServer(deps) {
51907
53209
  };
51908
53210
  }
51909
53211
 
53212
+ // ../server/src/lifecycle/policy-reconciliation.ts
53213
+ import { readFileSync as readFileSync5 } from "fs";
53214
+
53215
+ class PolicyReconciliation {
53216
+ store;
53217
+ url;
53218
+ token;
53219
+ active;
53220
+ constructor(store, url2, token) {
53221
+ this.store = store;
53222
+ this.url = url2;
53223
+ this.token = token;
53224
+ }
53225
+ async post(path, body) {
53226
+ try {
53227
+ const response = await fetch(`${this.url.replace(/\/$/, "")}/${path}`, {
53228
+ method: "POST",
53229
+ redirect: "error",
53230
+ signal: AbortSignal.timeout(5000),
53231
+ headers: { authorization: `Bearer ${this.token}`, "content-type": "application/json" },
53232
+ body: JSON.stringify(body)
53233
+ });
53234
+ if (!response.ok)
53235
+ throw new Error("policy response");
53236
+ return await response.json();
53237
+ } catch {
53238
+ throw new Error("Policy reconciliation unavailable");
53239
+ }
53240
+ }
53241
+ tick() {
53242
+ this.active ??= this.reconcile().finally(() => {
53243
+ this.active = undefined;
53244
+ });
53245
+ return this.active;
53246
+ }
53247
+ async drain() {
53248
+ await this.active?.catch(() => {});
53249
+ }
53250
+ async reconcile() {
53251
+ const pending = await this.post("pending", {});
53252
+ if (typeof pending !== "object" || pending === null || !("workspace_ids" in pending) || !Array.isArray(pending.workspace_ids)) {
53253
+ throw new Error("Invalid policy reconciliation response");
53254
+ }
53255
+ let failed = false;
53256
+ for (const id of pending.workspace_ids) {
53257
+ try {
53258
+ if (typeof id !== "string")
53259
+ throw new Error("Invalid workspace identity");
53260
+ const row = await this.store.getWorkspace(id);
53261
+ if (!row)
53262
+ throw new Error("Missing reconciliation workspace");
53263
+ const ref = row.providerRef;
53264
+ const result = await this.post("reconcile", {
53265
+ workspace_id: row.id,
53266
+ principal_id: row.principalId,
53267
+ external_id: row.externalId,
53268
+ template_name: row.templateName,
53269
+ launch_mode: row.launchMode,
53270
+ metadata: row.metadata,
53271
+ state: row.state,
53272
+ reason_code: row.reasonCode,
53273
+ launch_attempts: row.launchAttempts,
53274
+ provider_kind: row.providerKind,
53275
+ provider_ref: ref ? {
53276
+ id: ref.id,
53277
+ namespace: ref.namespace,
53278
+ poolRuntimeId: ref.poolRuntimeId,
53279
+ terminationEvidence: ref.terminationEvidence
53280
+ } : null
53281
+ });
53282
+ if (typeof result !== "object" || result === null || !("workspace_id" in result) || result.workspace_id !== row.id || !("released" in result) || typeof result.released !== "boolean") {
53283
+ throw new Error("Invalid policy reconciliation response");
53284
+ }
53285
+ if ("reason" in result && ["termination-unproven", "provider-unavailable"].includes(String(result.reason)))
53286
+ failed = true;
53287
+ } catch {
53288
+ failed = true;
53289
+ }
53290
+ }
53291
+ if (failed)
53292
+ throw new Error("Policy reconciliation unavailable or termination unproven");
53293
+ }
53294
+ }
53295
+ function loadPolicyReconciliation(store, config2) {
53296
+ if (!config2)
53297
+ return;
53298
+ const token = readFileSync5(config2.tokenFile, "utf8").trim();
53299
+ if (token.length < 24 || /\s/.test(token))
53300
+ throw new Error("Invalid launch policy control token");
53301
+ return new PolicyReconciliation(store, config2.url, token);
53302
+ }
53303
+
51910
53304
  // ../server/src/lifecycle/lifecycle.ts
51911
53305
  var defaultLog = (message) => console.log(`[pocketcoder-server] ${message}`);
51912
53306
  async function initializeStore(config2, log) {
@@ -51944,6 +53338,7 @@ function createWorkspaceDriver(config2) {
51944
53338
  return new KubernetesDriver({
51945
53339
  ...egress,
51946
53340
  namespace: config2.kubernetesNamespace,
53341
+ captureTerminationEvidence: Boolean(config2.launchPolicy),
51947
53342
  nodeSelector: config2.kubernetesNodeSelector ?? undefined,
51948
53343
  tolerations: config2.kubernetesTolerations,
51949
53344
  ...config2.kubernetesServiceAccount ? { serviceAccountName: config2.kubernetesServiceAccount } : {}
@@ -52006,6 +53401,7 @@ function startExclusiveTimer(intervalMs, task, errorContext, log) {
52006
53401
  }
52007
53402
  async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
52008
53403
  const log = options.log ?? defaultLog;
53404
+ const authorizeLaunch = loadLaunchPolicy(config2.launchPolicy);
52009
53405
  const logger = createStructuredLogger((record3) => log(JSON.stringify(record3)));
52010
53406
  const metrics = new RuntimeMetrics;
52011
53407
  log(`config: ${JSON.stringify(configSummary(config2))}`);
@@ -52021,7 +53417,11 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
52021
53417
  const storageDriver = createStorageDriver(config2);
52022
53418
  const secretResolver = createSecretResolver(config2);
52023
53419
  const readiness = new Readiness({ reconciliation: "pending" }, metrics);
53420
+ const policyReconciliation = loadPolicyReconciliation(store, config2.launchPolicy);
53421
+ if (policyReconciliation)
53422
+ readiness.set("policy-reconciliation", "pending");
52024
53423
  const { app, websocket: websocket2, scheduler, persistence: persistence2, warmPool } = buildServer({
53424
+ ...authorizeLaunch ? { authorizeLaunch } : {},
52025
53425
  store,
52026
53426
  driver,
52027
53427
  ...storageDriver ? { storageDriver } : {},
@@ -52048,6 +53448,9 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
52048
53448
  const schedulerTimer = startExclusiveTimer(config2.schedulerIntervalMs, async () => {
52049
53449
  try {
52050
53450
  await scheduler.tick();
53451
+ const pendingPurges = await persistence2.retryPurges();
53452
+ readiness.set("cleanup", pendingPurges > 0 ? "pending" : "ok");
53453
+ metrics.observe("purge.pending", pendingPurges);
52051
53454
  readiness.set("coordinator", "ok");
52052
53455
  } catch (error51) {
52053
53456
  readiness.set("coordinator", "failed");
@@ -52055,6 +53458,15 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
52055
53458
  }
52056
53459
  }, "scheduler tick failed", log);
52057
53460
  const outboxTimer = startExclusiveTimer(config2.outboxIntervalMs, () => outbox2.tick(), "outbox tick failed", log);
53461
+ const policyTimer = policyReconciliation ? startExclusiveTimer(config2.schedulerIntervalMs, async () => {
53462
+ try {
53463
+ await policyReconciliation.tick();
53464
+ readiness.set("policy-reconciliation", "ok");
53465
+ } catch (error51) {
53466
+ readiness.set("policy-reconciliation", "failed");
53467
+ throw error51;
53468
+ }
53469
+ }, "policy reconciliation failed", log) : null;
52058
53470
  const warmPoolTimer = warmPool && warmPoolContinuously ? startExclusiveTimer(config2.schedulerIntervalMs, () => warmPool.reconcile(), "warm pool reconciliation failed", log) : null;
52059
53471
  const retentionTimer = startExclusiveTimer(60000, async () => {
52060
53472
  const { deleted, skipped } = await persistence2.pruneExpired();
@@ -52075,6 +53487,9 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
52075
53487
  clearInterval(schedulerTimer);
52076
53488
  clearInterval(outboxTimer);
52077
53489
  clearInterval(retentionTimer);
53490
+ if (policyTimer)
53491
+ clearInterval(policyTimer);
53492
+ await policyReconciliation?.drain();
52078
53493
  if (warmPoolTimer)
52079
53494
  clearInterval(warmPoolTimer);
52080
53495
  throw error51;
@@ -52096,6 +53511,9 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
52096
53511
  clearInterval(schedulerTimer);
52097
53512
  clearInterval(outboxTimer);
52098
53513
  clearInterval(retentionTimer);
53514
+ if (policyTimer)
53515
+ clearInterval(policyTimer);
53516
+ await policyReconciliation?.drain();
52099
53517
  if (warmPoolTimer)
52100
53518
  clearInterval(warmPoolTimer);
52101
53519
  await server.stop(true);
@@ -52146,7 +53564,7 @@ function stateFromJson(value) {
52146
53564
  }
52147
53565
  function readState() {
52148
53566
  try {
52149
- return stateFromJson(JSON.parse(readFileSync4(statePath(), "utf8")));
53567
+ return stateFromJson(JSON.parse(readFileSync6(statePath(), "utf8")));
52150
53568
  } catch {
52151
53569
  return null;
52152
53570
  }
@@ -52154,7 +53572,7 @@ function readState() {
52154
53572
  function writeState(state) {
52155
53573
  const path = statePath();
52156
53574
  mkdirSync(dirname7(path), { recursive: true, mode: 448 });
52157
- const temporary = `${path}.${process.pid}.${randomUUID26()}.tmp`;
53575
+ const temporary = `${path}.${process.pid}.${randomUUID28()}.tmp`;
52158
53576
  writeFileSync(temporary, `${JSON.stringify(state, null, 2)}
52159
53577
  `, { mode: 384 });
52160
53578
  renameSync(temporary, path);
@@ -52226,7 +53644,7 @@ function selfInvocation(args) {
52226
53644
  }
52227
53645
  function logTail(path) {
52228
53646
  try {
52229
- const content = readFileSync4(path, "utf8");
53647
+ const content = readFileSync6(path, "utf8");
52230
53648
  return content.slice(Math.max(0, content.length - 4000)).trim();
52231
53649
  } catch {
52232
53650
  return "";
@@ -52263,7 +53681,7 @@ async function startManagedServer(options) {
52263
53681
  if (existing)
52264
53682
  removeState();
52265
53683
  const config2 = loadConfig();
52266
- const instanceToken = randomUUID26();
53684
+ const instanceToken = randomUUID28();
52267
53685
  const state = {
52268
53686
  version: 1,
52269
53687
  pid: 0,
@@ -52442,7 +53860,7 @@ function addStorageCommands(parser2) {
52442
53860
  }
52443
53861
 
52444
53862
  // src/commands/templates/list.ts
52445
- function addListCommand4(parser2) {
53863
+ function addListCommand5(parser2) {
52446
53864
  return addAction(parser2, "list", "List authorized template versions through the REST API", (command2) => command2.option("json", { type: "boolean", description: "Print JSON" }), async (flags) => {
52447
53865
  const items = await controlPlaneClient().templates.list();
52448
53866
  if (flags.json)
@@ -52535,7 +53953,7 @@ function addValidateCommand(parser2) {
52535
53953
 
52536
53954
  // src/commands/templates/index.ts
52537
53955
  function addTemplateCommands(parser2) {
52538
- return addResource(parser2, "templates", "Validate and inspect templates", (commands) => addListDatabaseCommand(addListCommand4(addRenderCommand(addValidateCommand(commands)))));
53956
+ return addResource(parser2, "templates", "Validate and inspect templates", (commands) => addListDatabaseCommand(addListCommand5(addRenderCommand(addValidateCommand(commands)))));
52539
53957
  }
52540
53958
 
52541
53959
  // src/commands/workspaces/chat-session.ts
@@ -52567,8 +53985,8 @@ async function waitForAgentInput(id, options, deps) {
52567
53985
  }
52568
53986
 
52569
53987
  // src/commands/workspaces/attachments.ts
52570
- import { randomUUID as randomUUID27 } from "crypto";
52571
- import { readFileSync as readFileSync5, statSync as statSync4 } from "fs";
53988
+ import { randomUUID as randomUUID29 } from "crypto";
53989
+ import { readFileSync as readFileSync7, statSync as statSync4 } from "fs";
52572
53990
  import { basename as basename2, extname as extname2 } from "path";
52573
53991
  var MEDIA_TYPES = {
52574
53992
  ".csv": "text/csv",
@@ -52600,12 +54018,12 @@ async function uploadAttachments(api3, workspaceId, paths, log) {
52600
54018
  const name2 = basename2(path);
52601
54019
  let bytes;
52602
54020
  try {
52603
- bytes = readFileSync5(path);
54021
+ bytes = readFileSync7(path);
52604
54022
  } catch {
52605
54023
  throw new Error(`could not read attachment file: ${path}`);
52606
54024
  }
52607
54025
  log(`uploading ${name2} (${bytes.byteLength} bytes)`);
52608
- const id = randomUUID27();
54026
+ const id = randomUUID29();
52609
54027
  const response = await api3(`/v1/workspaces/${workspaceId}/attachments/${id}`, {
52610
54028
  method: "PUT",
52611
54029
  headers: {
@@ -52673,7 +54091,7 @@ function fileFlags(value) {
52673
54091
  }
52674
54092
 
52675
54093
  // src/commands/workspaces/chat-cursor.ts
52676
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
54094
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
52677
54095
  import { homedir as homedir2 } from "os";
52678
54096
  import { dirname as dirname8, join as join10 } from "path";
52679
54097
  function cursorFile() {
@@ -52682,7 +54100,7 @@ function cursorFile() {
52682
54100
  }
52683
54101
  function readCursors(path) {
52684
54102
  try {
52685
- return JSON.parse(readFileSync6(path, "utf8"));
54103
+ return JSON.parse(readFileSync8(path, "utf8"));
52686
54104
  } catch {
52687
54105
  return {};
52688
54106
  }
@@ -53050,7 +54468,7 @@ function addChatCommand(parser2) {
53050
54468
  }
53051
54469
 
53052
54470
  // src/commands/workspaces/create.ts
53053
- import { randomUUID as randomUUID28 } from "crypto";
54471
+ import { randomUUID as randomUUID30 } from "crypto";
53054
54472
 
53055
54473
  // src/commands/workspaces/launch-input.ts
53056
54474
  function parseLaunchInput(value, fail2) {
@@ -53157,7 +54575,7 @@ async function waitForReady(initial, flags, { client, fail: failCommand }) {
53157
54575
  }
53158
54576
  async function createWorkspace(flags, deps) {
53159
54577
  const template = required2(flags, "template", deps.fail);
53160
- const externalId = typeof flags["external-id"] === "string" ? flags["external-id"] : `pcd-${randomUUID28()}`;
54578
+ const externalId = typeof flags["external-id"] === "string" ? flags["external-id"] : `pcd-${randomUUID30()}`;
53161
54579
  const launchInput = parseLaunchInput(flags.input, deps.fail);
53162
54580
  const created = await deps.client.workspaces.create({
53163
54581
  externalId,
@@ -53226,7 +54644,7 @@ async function listWorkspaces(flags) {
53226
54644
  else
53227
54645
  printWorkspaces(items);
53228
54646
  }
53229
- function addListCommand5(parser2) {
54647
+ function addListCommand6(parser2) {
53230
54648
  return addAction(parser2, "list", "List workspaces", (command2) => command2.option("active", { type: "boolean", description: "Only show nonterminal workspaces" }).option("state", { type: "string", description: "Filter by state" }).option("template", { type: "string", description: "Filter by template name" }).option("external-id", { type: "string", description: "Filter by external ID" }).option("limit", { type: "string", description: "Maximum number of workspaces" }).option("json", { type: "boolean", description: "Print JSON" }), listWorkspaces);
53231
54649
  }
53232
54650
 
@@ -53269,14 +54687,29 @@ function addOutputsCommand(parser2) {
53269
54687
  }
53270
54688
 
53271
54689
  // src/commands/workspaces/preserve.ts
53272
- import { randomUUID as randomUUID29 } from "crypto";
54690
+ import { randomUUID as randomUUID31 } from "crypto";
53273
54691
  function addPreserveCommand(parser2) {
53274
54692
  return addAction(parser2, "preserve", "Stop and checkpoint a persistence-enabled workspace", (command2) => command2.option("id", { type: "string", demandOption: true, description: "Workspace ID" }).option("retention", { type: "string" }).option("label", { type: "string" }), async (flags) => {
53275
54693
  const id = need(flags, "id");
53276
54694
  const result = await controlPlaneClient().workspaces.preserve(id, {
53277
54695
  ...typeof flags.retention === "string" ? { retention: flags.retention } : {},
53278
54696
  ...typeof flags.label === "string" ? { label: flags.label } : {}
53279
- }, `preserve-${id}-${randomUUID29()}`);
54697
+ }, `preserve-${id}-${randomUUID31()}`);
54698
+ console.log(JSON.stringify(result, null, 2));
54699
+ });
54700
+ }
54701
+
54702
+ // src/commands/workspaces/purge.ts
54703
+ function addPurgeCommand(parser2) {
54704
+ return addAction(parser2, "purge", "Purge owned workspace content and return a durable operation", (command2) => command2.option("id", { type: "string", demandOption: true, description: "Workspace ID" }).option("request-id", {
54705
+ type: "string",
54706
+ demandOption: true,
54707
+ description: "Stable request ID; use a new ID for backup replay"
54708
+ }).option("principal-id", { type: "string", description: "Explicit target for delegated operator recovery" }), async (flags) => {
54709
+ const client = controlPlaneClient();
54710
+ const id = need(flags, "id");
54711
+ const key = need(flags, "request-id");
54712
+ const result = typeof flags["principal-id"] === "string" ? await client.recovery.purge(flags["principal-id"], id, key) : await client.workspaces.purge(id, key);
53280
54713
  console.log(JSON.stringify(result, null, 2));
53281
54714
  });
53282
54715
  }
@@ -53451,7 +54884,7 @@ function addTerminalSessionsCommand(parser2) {
53451
54884
  // src/commands/workspaces/index.ts
53452
54885
  function addWorkspaceCommands(parser2) {
53453
54886
  const actions = [
53454
- addListCommand5,
54887
+ addListCommand6,
53455
54888
  addCreateCommand2,
53456
54889
  addGetCommand2,
53457
54890
  addLogsCommand,
@@ -53460,6 +54893,7 @@ function addWorkspaceCommands(parser2) {
53460
54893
  addTerminalCommand,
53461
54894
  addCancelCommand,
53462
54895
  addPreserveCommand,
54896
+ addPurgeCommand,
53463
54897
  addRestoreCommand,
53464
54898
  addRecreateCommand,
53465
54899
  addOutputsCommand,