@pstdio/pocketcoder-cli 0.7.2 → 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.2";
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);
@@ -21248,6 +21262,17 @@ function sqlValues(values) {
21248
21262
  }
21249
21263
  var timestamptz = (name2) => timestamp2(name2, { withTimezone: true, mode: "date" });
21250
21264
 
21265
+ // ../db/src/schema/structured-json.ts
21266
+ var structuredJson = customType2({
21267
+ dataType: () => "jsonb",
21268
+ codec: "jsonb",
21269
+ fromDriver: (value) => typeof value === "string" ? JSON.parse(value) : value
21270
+ });
21271
+ function structuredJsonValue(column) {
21272
+ return sql`CASE WHEN jsonb_typeof(${column}) = 'string'
21273
+ THEN (${column} #>> '{}')::jsonb ELSE ${column} END`;
21274
+ }
21275
+
21251
21276
  // ../db/src/schema/access.ts
21252
21277
  function createAccessTables(table2 = pgTable) {
21253
21278
  const templates2 = table2("templates", {
@@ -21256,7 +21281,7 @@ function createAccessTables(table2 = pgTable) {
21256
21281
  version: text2("version").notNull(),
21257
21282
  digest: text2("digest").notNull().unique(),
21258
21283
  description: text2("description"),
21259
- spec: jsonb("spec").$type().notNull(),
21284
+ spec: structuredJson("spec").$type().notNull(),
21260
21285
  status: text2("status").$type().notNull(),
21261
21286
  createdAt: timestamptz("created_at").notNull(),
21262
21287
  retiredAt: timestamptz("retired_at")
@@ -21273,6 +21298,9 @@ function createAccessTables(table2 = pgTable) {
21273
21298
  createdAt: timestamptz("created_at").notNull()
21274
21299
  });
21275
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([]),
21276
21304
  id: uuid("id").primaryKey(),
21277
21305
  principalId: uuid("principal_id").notNull().references(() => principals.id),
21278
21306
  secretDigest: bytea2("secret_digest").$type().notNull(),
@@ -21281,7 +21309,7 @@ function createAccessTables(table2 = pgTable) {
21281
21309
  expiresAt: timestamptz("expires_at"),
21282
21310
  revokedAt: timestamptz("revoked_at"),
21283
21311
  lastUsedAt: timestamptz("last_used_at")
21284
- });
21312
+ }, (table3) => [unique().on(table3.principalId, table3.issuanceRequestId)]);
21285
21313
  return { templates: templates2, principals, machineKeys };
21286
21314
  }
21287
21315
 
@@ -35629,6 +35657,58 @@ var AttachmentResolvedPayload = exports_external.object({
35629
35657
  descriptors: exports_external.array(AttachmentDescriptorSchema).optional(),
35630
35658
  missing_id: exports_external.uuid().optional()
35631
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() });
35632
35712
  // ../contracts/src/common/canonical.ts
35633
35713
  import { createHash as createHash2 } from "crypto";
35634
35714
  function canonicalJson(value) {
@@ -35684,6 +35764,8 @@ var ERROR_CODES = {
35684
35764
  "auth.invalid_key": 401,
35685
35765
  "auth.missing_scope": 403,
35686
35766
  "auth.disabled_principal": 403,
35767
+ "principal.not_found": 404,
35768
+ "key.not_found": 404,
35687
35769
  "validation.invalid": 400,
35688
35770
  "idempotency.conflict": 409,
35689
35771
  "capacity.queue_full": 429,
@@ -35771,33 +35853,6 @@ function CursorQuerySchema(maxLimit, defaultLimit) {
35771
35853
  limit: exports_external.coerce.number().int().positive().max(maxLimit).default(defaultLimit)
35772
35854
  });
35773
35855
  }
35774
- // ../contracts/src/common/scopes.ts
35775
- var SCOPES = [
35776
- "templates:read",
35777
- "workspaces:create",
35778
- "workspaces:read",
35779
- "workspaces:cancel",
35780
- "workspaces:preserve",
35781
- "workspaces:restore",
35782
- "checkpoints:read",
35783
- "checkpoints:delete",
35784
- "outputs:read",
35785
- "conversations:read",
35786
- "conversations:delete",
35787
- "services:relay",
35788
- "attachments:write",
35789
- "logs:read",
35790
- "network:read",
35791
- "terminal:attach",
35792
- "terminal:read",
35793
- "admin"
35794
- ];
35795
- function isScope(value) {
35796
- return SCOPES.includes(value);
35797
- }
35798
- function hasScope(granted, required2) {
35799
- return granted.includes(required2) || granted.includes("admin");
35800
- }
35801
35856
  // ../contracts/src/conversations/conversation.ts
35802
35857
  var CONVERSATION_ROLES = ["user", "assistant", "system", "tool"];
35803
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");
@@ -35924,7 +35979,7 @@ var CHECKPOINT_STATES = ["creating", "ready", "failed", "deleting", "deleted"];
35924
35979
  function isCheckpointState(value) {
35925
35980
  return CHECKPOINT_STATES.some((state) => state === value);
35926
35981
  }
35927
- var OPERATION_KINDS = ["preserve", "restore", "verify", "delete"];
35982
+ var OPERATION_KINDS = ["preserve", "restore", "verify", "delete", "purge"];
35928
35983
  var OPERATION_STATES = ["pending", "running", "succeeded", "failed"];
35929
35984
  var STORAGE_STATES = [
35930
35985
  "allocating",
@@ -37314,7 +37369,7 @@ function createActivityTables(table2 = pgTable, { workspaces: workspaces2 }) {
37314
37369
  role: text2("role").$type().notNull(),
37315
37370
  content: text2("content").notNull(),
37316
37371
  occurredAt: timestamptz("occurred_at").notNull(),
37317
- metadata: jsonb("metadata").$type().notNull().default({}),
37372
+ metadata: structuredJson("metadata").$type().notNull().default({}),
37318
37373
  createdAt: timestamptz("created_at").notNull()
37319
37374
  }, (table3) => [
37320
37375
  primaryKey({ columns: [table3.workspaceId, table3.seq] }),
@@ -37368,9 +37423,9 @@ function createPersistenceTables(table2 = pgTable, { principals }, { workspaces:
37368
37423
  workspaceId: uuid("workspace_id").notNull().references(() => workspaces2.id),
37369
37424
  principalId: uuid("principal_id").notNull().references(() => principals.id),
37370
37425
  providerKind: text2("provider_kind").notNull(),
37371
- providerRef: jsonb("provider_ref").$type().notNull(),
37426
+ providerRef: structuredJson("provider_ref").$type().notNull(),
37372
37427
  state: text2("state").$type().notNull(),
37373
- mountManifest: jsonb("mount_manifest").$type().notNull(),
37428
+ mountManifest: structuredJson("mount_manifest").$type().notNull(),
37374
37429
  logicalBytes: bigint2("logical_bytes", { mode: "number" }),
37375
37430
  fileCount: bigint2("file_count", { mode: "number" }),
37376
37431
  retainedUntil: timestamptz("retained_until"),
@@ -37393,11 +37448,11 @@ function createPersistenceTables(table2 = pgTable, { principals }, { workspaces:
37393
37448
  state: text2("state").$type().notNull(),
37394
37449
  reasonCode: text2("reason_code").$type(),
37395
37450
  providerKind: text2("provider_kind").notNull(),
37396
- providerRef: jsonb("provider_ref").$type(),
37397
- templateSnapshot: jsonb("template_snapshot").$type().notNull(),
37451
+ providerRef: structuredJson("provider_ref").$type(),
37452
+ templateSnapshot: structuredJson("template_snapshot").$type().notNull(),
37398
37453
  templateDigest: text2("template_digest").notNull(),
37399
- sourceProvenance: jsonb("source_provenance").$type(),
37400
- manifest: jsonb("manifest").$type(),
37454
+ sourceProvenance: structuredJson("source_provenance").$type(),
37455
+ manifest: structuredJson("manifest").$type(),
37401
37456
  manifestDigest: text2("manifest_digest"),
37402
37457
  logicalBytes: bigint2("logical_bytes", { mode: "number" }),
37403
37458
  storedBytes: bigint2("stored_bytes", { mode: "number" }),
@@ -37452,7 +37507,7 @@ function createWorkspaceTables(table2 = pgTable, { principals, machineKeys, temp
37452
37507
  templateName: text2("template_name").notNull(),
37453
37508
  templateVersion: text2("template_version").notNull(),
37454
37509
  templateDigest: text2("template_digest").notNull(),
37455
- templateSnapshot: jsonb("template_snapshot").$type().notNull(),
37510
+ templateSnapshot: structuredJson("template_snapshot").$type().notNull(),
37456
37511
  state: text2("state").$type().notNull(),
37457
37512
  reasonCode: text2("reason_code").$type(),
37458
37513
  agentState: text2("agent_state").$type().notNull().default("unknown"),
@@ -37463,9 +37518,9 @@ function createWorkspaceTables(table2 = pgTable, { principals, machineKeys, temp
37463
37518
  failureLogTailTruncated: boolean2("failure_log_tail_truncated").notNull().default(false),
37464
37519
  failureLastLogSeq: bigint2("failure_last_log_seq", { mode: "number" }),
37465
37520
  terminalIntent: text2("terminal_intent").$type(),
37466
- launchInput: jsonb("launch_input").$type(),
37521
+ launchInput: structuredJson("launch_input").$type(),
37467
37522
  providerKind: text2("provider_kind"),
37468
- providerRef: jsonb("provider_ref").$type(),
37523
+ providerRef: structuredJson("provider_ref").$type(),
37469
37524
  provisioningMode: text2("provisioning_mode").$type(),
37470
37525
  registrationDigest: bytea2("registration_digest").$type(),
37471
37526
  registrationExpiresAt: timestamptz("registration_expires_at"),
@@ -37476,20 +37531,21 @@ function createWorkspaceTables(table2 = pgTable, { principals, machineKeys, temp
37476
37531
  readyAt: timestamptz("ready_at"),
37477
37532
  lastActivityAt: timestamptz("last_activity_at"),
37478
37533
  launchAttempts: integer("launch_attempts").notNull().default(0),
37479
- health: jsonb("health").$type().notNull().default({}),
37480
- metadata: jsonb("metadata").$type().notNull().default({}),
37534
+ health: structuredJson("health").$type().notNull().default({}),
37535
+ metadata: structuredJson("metadata").$type().notNull().default({}),
37481
37536
  deadlineAt: timestamptz("deadline_at").notNull(),
37482
37537
  createdAt: timestamptz("created_at").notNull(),
37483
37538
  updatedAt: timestamptz("updated_at").notNull(),
37484
37539
  terminalAt: timestamptz("terminal_at"),
37540
+ purgeRequestedAt: timestamptz("purge_requested_at"),
37485
37541
  originWorkspaceId: uuid("origin_workspace_id"),
37486
37542
  restoredFromCheckpointId: uuid("restored_from_checkpoint_id"),
37487
- sourceDescriptor: jsonb("source_descriptor").$type(),
37488
- resolvedSource: jsonb("resolved_source").$type(),
37543
+ sourceDescriptor: structuredJson("source_descriptor").$type(),
37544
+ resolvedSource: structuredJson("resolved_source").$type(),
37489
37545
  persistenceCapability: text2("persistence_capability").$type().notNull().default("filesystem_only"),
37490
37546
  latestCheckpointId: uuid("latest_checkpoint_id"),
37491
37547
  launchMode: text2("launch_mode").$type().notNull().default("create"),
37492
- outputs: jsonb("outputs").$type().notNull().default({})
37548
+ outputs: structuredJson("outputs").$type().notNull().default({})
37493
37549
  }, (table3) => [
37494
37550
  check("workspaces_state_check", sql`${table3.state} IN ${sqlValues(WORKSPACE_STATES)}`),
37495
37551
  check("workspaces_reason_code_check", sql`${table3.reasonCode} IS NULL OR ${table3.reasonCode} IN ${sqlValues(REASON_CODES)}`),
@@ -37552,7 +37608,7 @@ function createWorkspaceTables(table2 = pgTable, { principals, machineKeys, temp
37552
37608
  driverKind: text2("driver_kind").notNull(),
37553
37609
  eligibilityFingerprint: text2("eligibility_fingerprint").notNull(),
37554
37610
  state: text2("state").$type().notNull(),
37555
- providerRef: jsonb("provider_ref").$type(),
37611
+ providerRef: structuredJson("provider_ref").$type(),
37556
37612
  enrollmentDigest: bytea2("enrollment_digest").$type(),
37557
37613
  enrollmentExpiresAt: timestamptz("enrollment_expires_at"),
37558
37614
  workspaceId: uuid("workspace_id").references(() => workspaces2.id),
@@ -37601,9 +37657,9 @@ var {
37601
37657
  } = createSchema();
37602
37658
  // ../db/src/database/context.ts
37603
37659
  var {SQL: SQL6 } = globalThis.Bun;
37604
- function createDatabaseContext(databaseUrl, schemaName) {
37660
+ function createDatabaseContext(databaseUrl, schemaName, options = {}) {
37605
37661
  const schema = assertValidSchema(schemaName);
37606
- const client = new SQL6(databaseUrl);
37662
+ const client = new SQL6(databaseUrl, options);
37607
37663
  return {
37608
37664
  schema,
37609
37665
  client,
@@ -37715,9 +37771,63 @@ function requiredRow(row) {
37715
37771
  return row;
37716
37772
  }
37717
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
+
37718
37819
  // ../db/src/modules/auth/repository.ts
37719
- function createAuth({ db, tables: { principals: principals2, machineKeys: machineKeys2 } }) {
37820
+ function createAuth(context) {
37821
+ const {
37822
+ db,
37823
+ tables: { principals: principals2, machineKeys: machineKeys2 }
37824
+ } = context;
37720
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
+ },
37721
37831
  async createPrincipal(name2, scopes, templateNames) {
37722
37832
  const [row] = await db.insert(principals2).values({ id: randomUUID2(), name: name2, scopes, templateNames, createdAt: sql`now()` }).returning();
37723
37833
  return requiredRow(row);
@@ -37736,9 +37846,6 @@ function createAuth({ db, tables: { principals: principals2, machineKeys: machin
37736
37846
  async setPrincipalDisabled(id, disabled) {
37737
37847
  await db.update(principals2).set({ disabledAt: disabled ? sql`now()` : null }).where(eq(principals2.id, id));
37738
37848
  },
37739
- async insertMachineKey(row) {
37740
- await db.insert(machineKeys2).values(row);
37741
- },
37742
37849
  async getMachineKeyWithPrincipal(keyId) {
37743
37850
  const [row] = await db.select({ key: machineKeys2, principal: principals2 }).from(machineKeys2).innerJoin(principals2, eq(principals2.id, machineKeys2.principalId)).where(eq(machineKeys2.id, keyId));
37744
37851
  return row ?? null;
@@ -37753,16 +37860,71 @@ function createAuth({ db, tables: { principals: principals2, machineKeys: machin
37753
37860
  };
37754
37861
  }
37755
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
+
37756
37916
  // ../db/src/modules/conversations/repository.ts
37757
37917
  var MAX_CONVERSATION_BYTES = 50 * 1024 * 1024;
37758
37918
  var MAX_CONVERSATION_MESSAGES = 1e5;
37759
- function createConversations({
37760
- db,
37761
- tables: { workspaceConversations: conversations2, workspaceConversationMessages: messages }
37762
- }) {
37919
+ function createConversations(context) {
37920
+ const {
37921
+ db,
37922
+ tables: { workspaceConversations: conversations2, workspaceConversationMessages: messages }
37923
+ } = context;
37763
37924
  return {
37764
37925
  async appendConversationMessage(input) {
37765
37926
  return db.transaction(async (tx) => {
37927
+ await requireContentWritable(tx, context.tables, input.workspaceId);
37766
37928
  await lock(tx, input.workspaceId, 7081);
37767
37929
  const [state] = await tx.select({ status: conversations2.status }).from(conversations2).where(eq(conversations2.workspaceId, input.workspaceId)).for("update");
37768
37930
  if (state?.status === "deleted")
@@ -37816,12 +37978,18 @@ function createConversations({
37816
37978
 
37817
37979
  // ../db/src/modules/logs/repository.ts
37818
37980
  var MAX_LOG_BYTES = 10 * 1024 * 1024;
37819
- function createLogs({ db, tables: { workspaceLogs: logs2 } }) {
37981
+ function createLogs(context) {
37982
+ const {
37983
+ db,
37984
+ tables: { workspaceLogs: logs2 }
37985
+ } = context;
37820
37986
  return {
37821
37987
  async appendLogs(workspaceId, entries) {
37822
37988
  if (entries.length === 0)
37823
37989
  return;
37824
37990
  await db.transaction(async (tx) => {
37991
+ if (!await contentWritable(tx, context.tables, workspaceId))
37992
+ return;
37825
37993
  await lock(tx, workspaceId, 7080);
37826
37994
  const [stats] = await tx.select({
37827
37995
  maxSeq: sql`coalesce(max(${logs2.seq}),0)`.mapWith(Number),
@@ -37861,12 +38029,18 @@ function createLogs({ db, tables: { workspaceLogs: logs2 } }) {
37861
38029
  }
37862
38030
 
37863
38031
  // ../db/src/modules/network-audit/repository.ts
37864
- function createNetworkAudit({ db, tables: { workspaceNetworkEvents: events, workspaces: workspaces3 } }) {
38032
+ function createNetworkAudit(context) {
38033
+ const {
38034
+ db,
38035
+ tables: { workspaceNetworkEvents: events, workspaces: workspaces3 }
38036
+ } = context;
37865
38037
  return {
37866
38038
  async appendNetworkEvents(workspaceId, sourceSessionId, inputs) {
37867
38039
  if (inputs.length === 0)
37868
38040
  return;
37869
38041
  await db.transaction(async (tx) => {
38042
+ if (!await contentWritable(tx, context.tables, workspaceId))
38043
+ return;
37870
38044
  await lock(tx, workspaceId, 7348);
37871
38045
  for (const event of inputs) {
37872
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)));
@@ -37908,7 +38082,11 @@ function createNetworkAudit({ db, tables: { workspaceNetworkEvents: events, work
37908
38082
  // ../db/src/modules/outbox/repository.ts
37909
38083
  import { randomUUID as randomUUID3 } from "crypto";
37910
38084
  var CLAIM_LEASE_MS = 60000;
37911
- function createOutbox({ db, tables: { eventOutbox: outbox2 } }) {
38085
+ function createOutbox(context) {
38086
+ const {
38087
+ db,
38088
+ tables: { eventOutbox: outbox2 }
38089
+ } = context;
37912
38090
  return {
37913
38091
  async claimDueEvents(now, limit) {
37914
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 });
@@ -37922,29 +38100,38 @@ function createOutbox({ db, tables: { eventOutbox: outbox2 } }) {
37922
38100
  await db.update(outbox2).set({ attemptCount: sql`${outbox2.attemptCount}+1`, lastErrorCode: errorCode, nextAttemptAt }).where(eq(outbox2.id, id));
37923
38101
  },
37924
38102
  async appendEvent(workspaceId, eventType, payload, at) {
37925
- await db.insert(outbox2).values({
37926
- id: randomUUID3(),
37927
- workspaceId,
37928
- eventType,
37929
- payload: payload === null ? sql`'null'::jsonb` : payload,
37930
- occurredAt: at,
37931
- 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
+ });
37932
38114
  });
37933
38115
  }
37934
38116
  };
37935
38117
  }
37936
38118
 
37937
38119
  // ../db/src/modules/outputs/repository.ts
37938
- function createOutputs({ db, tables: { workspaceOutputs: outputs2, workspaces: workspaces3 } }) {
38120
+ function createOutputs(context) {
38121
+ const {
38122
+ db,
38123
+ tables: { workspaceOutputs: outputs2, workspaces: workspaces3 }
38124
+ } = context;
37939
38125
  return {
37940
38126
  async appendOutput(input) {
37941
38127
  return db.transaction(async (tx) => {
38128
+ await requireContentWritable(tx, context.tables, input.workspaceId);
37942
38129
  await lock(tx, input.workspaceId, 7081);
37943
38130
  const [latest] = await tx.select({ seq: sql`coalesce(max(${outputs2.seq}),0)`.mapWith(Number) }).from(outputs2).where(eq(outputs2.workspaceId, input.workspaceId));
37944
38131
  const row = { ...input, seq: requiredRow(latest).seq + 1 };
37945
38132
  await tx.insert(outputs2).values({ ...row, value: row.value === null ? sql`'null'::jsonb` : row.value });
37946
38133
  await tx.update(workspaces3).set({
37947
- outputs: sql`${workspaces3.outputs} || jsonb_build_object(${input.name}::text, ${sql.param(input.value, outputs2.value)}::jsonb)`,
38134
+ outputs: sql`${structuredJsonValue(workspaces3.outputs)} || jsonb_build_object(${input.name}::text, ${sql.param(input.value, outputs2.value)}::jsonb)`,
37948
38135
  updatedAt: input.occurredAt
37949
38136
  }).where(eq(workspaces3.id, input.workspaceId));
37950
38137
  return row;
@@ -37986,7 +38173,7 @@ function createCheckpoints({ db, tables: { workspaceCheckpoints: checkpoints } }
37986
38173
  function createOperations({
37987
38174
  db,
37988
38175
  schema,
37989
- tables: { workspaceOperations: operations, workspaceCheckpoints: checkpoints }
38176
+ tables: { workspaceOperations: operations, workspaceCheckpoints: checkpoints, workspaces: workspaces3 }
37990
38177
  }) {
37991
38178
  const incomplete = inArray(operations.state, ["pending", "running"]);
37992
38179
  async function countIncomplete(tx = db) {
@@ -38000,6 +38187,15 @@ function createOperations({
38000
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)));
38001
38188
  if (existing)
38002
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
+ }
38003
38199
  if (options.maxIncompleteOperations !== undefined && await countIncomplete(tx) >= options.maxIncompleteOperations)
38004
38200
  throw new OperationCapacityExceededError;
38005
38201
  const [row] = await tx.insert(operations).values(input).returning();
@@ -38118,6 +38314,152 @@ function createTerminals({ db, tables: { workspaceTerminalSessions: sessions } }
38118
38314
 
38119
38315
  // ../db/src/modules/workspaces/events.ts
38120
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
+ }
38121
38463
  // ../runtime-core/src/observability/metrics.ts
38122
38464
  function metricKey(name2, labels = {}) {
38123
38465
  const entries = Object.entries(labels).sort(([left2], [right2]) => left2.localeCompare(right2));
@@ -38213,6 +38555,25 @@ class OutboxDispatcher {
38213
38555
  await this.deps.store.markEventFailed(id, errorCode, new Date(this.now().getTime() + backoff));
38214
38556
  }
38215
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
+
38216
38577
  // ../runtime-core/src/reconciliation/reconciliation-metrics.ts
38217
38578
  async function measureReconciliation(metrics, kind, skipped, reconcile) {
38218
38579
  const startedAt = performance.now();
@@ -38267,8 +38628,7 @@ async function reconcileDeletion(deps, operation, checkpoint, now) {
38267
38628
  async function cleanupProvider(deps, workspace) {
38268
38629
  if (!workspace?.providerRef)
38269
38630
  return;
38270
- await deps.driver.stop(workspace.providerRef, 1).catch(() => {});
38271
- await deps.driver.remove(workspace.providerRef).catch(() => {});
38631
+ await stopWorkspaceProvider(deps.store, deps.driver, workspace, 1, deps.now?.() ?? new Date);
38272
38632
  }
38273
38633
  async function completePreserve(deps, operation, checkpoint, workspace, now) {
38274
38634
  await cleanupProvider(deps, workspace);
@@ -38303,13 +38663,26 @@ async function failPreserve(deps, operation, context, now) {
38303
38663
  }
38304
38664
  await failOperation(deps, operation, "checkpoint_failed", now);
38305
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
+ }
38306
38678
  async function reconcileOperation(deps, operation, now) {
38307
- const context = await loadOperationContext(deps, operation);
38679
+ if (operation.kind === "purge")
38680
+ return;
38308
38681
  if (operation.kind === "restore") {
38309
- if (!context.workspace)
38310
- await failOperation(deps, operation, "restore_failed", now);
38682
+ await reconcileRestore(deps, operation, now);
38311
38683
  return;
38312
38684
  }
38685
+ const context = await loadOperationContext(deps, operation);
38313
38686
  if (!context.checkpoint) {
38314
38687
  await failOperation(deps, operation, "checkpoint_storage_lost", now);
38315
38688
  return;
@@ -38361,22 +38734,13 @@ async function reconcilePersistence(deps) {
38361
38734
  }
38362
38735
  // ../runtime-core/src/reconciliation/reconcile.ts
38363
38736
  async function reconcileProviderRow(deps, row, found, now) {
38364
- if (row.state === "queued")
38737
+ if (row.state === "queued" || row.state === "terminating")
38365
38738
  return;
38366
38739
  const mismatched = found && found.templateDigest !== row.templateDigest;
38367
38740
  if (mismatched) {
38368
38741
  deps.log?.(`reconcile: template digest mismatch for ${row.id}; failing workspace`);
38369
38742
  }
38370
38743
  const lost = !found || mismatched;
38371
- if (lost && row.state === "terminating") {
38372
- await deps.store.transition(row.id, {
38373
- from: ["terminating"],
38374
- to: row.terminalIntent ?? "failed",
38375
- reason: row.reasonCode ?? "provider_lost",
38376
- at: now
38377
- });
38378
- return;
38379
- }
38380
38744
  if (lost && (row.state === "connected" || row.state === "ready")) {
38381
38745
  await settleLostStorage(deps, row, now);
38382
38746
  await deps.store.transition(row.id, {
@@ -38499,95 +38863,6 @@ async function loadTemplateDir(store, dir) {
38499
38863
  }
38500
38864
  return result;
38501
38865
  }
38502
- // ../auth/src/index.ts
38503
- import { createHmac, randomBytes, randomUUID as randomUUID5, timingSafeEqual } from "crypto";
38504
- var KEY_PREFIX = "pkt";
38505
- function issueMachineKey(pepper) {
38506
- const id = randomUUID5();
38507
- const secret = randomBytes(32).toString("base64url");
38508
- return {
38509
- id,
38510
- token: `${KEY_PREFIX}_${id}_${secret}`,
38511
- secretDigest: digestSecret(pepper, id, secret)
38512
- };
38513
- }
38514
- 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_-]+)$/;
38515
- function parseMachineKey(token) {
38516
- const match = KEY_RE.exec(token);
38517
- if (!match) {
38518
- return null;
38519
- }
38520
- return { id: match[1], secret: match[2] };
38521
- }
38522
- function digestSecret(pepper, keyId, secret) {
38523
- return new Uint8Array(createHmac("sha256", pepper).update(`${keyId}:${secret}`).digest());
38524
- }
38525
- function verifySecret(pepper, keyId, secret, storedDigest) {
38526
- const computed = digestSecret(pepper, keyId, secret);
38527
- if (computed.length !== storedDigest.length) {
38528
- return false;
38529
- }
38530
- return timingSafeEqual(computed, storedDigest);
38531
- }
38532
- function generateOpaqueSecret() {
38533
- return randomBytes(32).toString("base64url");
38534
- }
38535
- function digestOpaque(pepper, secret) {
38536
- return new Uint8Array(createHmac("sha256", pepper).update(secret).digest());
38537
- }
38538
- function verifyOpaque(pepper, secret, storedDigest) {
38539
- const computed = digestOpaque(pepper, secret);
38540
- if (computed.length !== storedDigest.length) {
38541
- return false;
38542
- }
38543
- return timingSafeEqual(computed, storedDigest);
38544
- }
38545
- function signEvent(signingKey, timestamp4, body) {
38546
- const mac3 = createHmac("sha256", signingKey).update(`${timestamp4}.${body}`).digest("hex");
38547
- return `sha256=${mac3}`;
38548
- }
38549
- var EGRESS_TOKEN_PREFIX = "pce1";
38550
- var EGRESS_TOKEN_DOMAIN = "pocketcoder-egress-audit-v1\x00";
38551
- function issueEgressAuditToken(signingKey, subject) {
38552
- const payload = Buffer.from(JSON.stringify({
38553
- v: 1,
38554
- aud: "egress-audit",
38555
- kind: subject.kind,
38556
- id: subject.id,
38557
- exp: subject.expiresAt.getTime()
38558
- })).toString("base64url");
38559
- const signature = createHmac("sha256", signingKey).update(`${EGRESS_TOKEN_DOMAIN}${payload}`).digest("base64url");
38560
- return `${EGRESS_TOKEN_PREFIX}.${payload}.${signature}`;
38561
- }
38562
- function verifyEgressAuditToken(signingKey, token, now = new Date) {
38563
- const [prefix, payload, signature, extra] = token.split(".");
38564
- if (prefix !== EGRESS_TOKEN_PREFIX || !payload || !signature || extra)
38565
- return null;
38566
- const expected = createHmac("sha256", signingKey).update(`${EGRESS_TOKEN_DOMAIN}${payload}`).digest("base64url");
38567
- const actualBytes = Buffer.from(signature);
38568
- const expectedBytes = Buffer.from(expected);
38569
- if (actualBytes.length !== expectedBytes.length || !timingSafeEqual(actualBytes, expectedBytes)) {
38570
- return null;
38571
- }
38572
- try {
38573
- const value = JSON.parse(Buffer.from(payload, "base64url").toString());
38574
- 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()) {
38575
- return null;
38576
- }
38577
- return { kind: value.kind, id: value.id };
38578
- } catch {
38579
- return null;
38580
- }
38581
- }
38582
- var REDACT_PATTERNS = [/pkt_[0-9a-f-]{36}_[A-Za-z0-9_-]+/g, /(authorization:?\s*bearer\s+)\S+/gi];
38583
- function redact(text6) {
38584
- let out = text6;
38585
- for (const pattern of REDACT_PATTERNS) {
38586
- out = out.replace(pattern, "[redacted]");
38587
- }
38588
- return out;
38589
- }
38590
-
38591
38866
  // ../runtime-core/src/scheduler/scheduler-base.ts
38592
38867
  var FAILURE_LOG_TAIL_BYTES = 16 * 1024;
38593
38868
  function decodeFailureLogTail(content, truncated) {
@@ -38774,8 +39049,20 @@ class SchedulerAdmission {
38774
39049
  return;
38775
39050
  }
38776
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
+ }
38777
39062
  async launch(row) {
38778
39063
  const { store, driver, secrets } = this.context.deps;
39064
+ if (!await this.authorized(row))
39065
+ return false;
38779
39066
  const now = this.context.now();
38780
39067
  const secret = secrets.generate();
38781
39068
  const registrationDigest = secrets.digest(secret);
@@ -38940,24 +39227,28 @@ class SchedulerLifecycle {
38940
39227
  connections.signal(row.id, "TERM");
38941
39228
  }
38942
39229
  if (updated.providerRef) {
38943
- 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));
38944
39231
  } else {
38945
39232
  await this.finalize(updated, terminalState, reason, at);
38946
39233
  }
38947
39234
  return updated;
38948
39235
  }
38949
- async finalize(row, terminalState, reason, at, retainStorage = false) {
39236
+ async finalize(row, terminalState, reason, at) {
38950
39237
  const { store, driver, connections } = this.context.deps;
39238
+ const retainStorage = terminalState === "failed" && row.templateSnapshot.spec.persistence.checkpoint.onFailure === "retain-for-recovery";
38951
39239
  if (row.providerRef) {
38952
39240
  try {
38953
- await driver.stop({ kind: row.providerKind ?? "", id: "", ...row.providerRef }, this.context.graceSeconds(row));
38954
- await driver.remove({
38955
- kind: row.providerKind ?? "",
38956
- id: "",
38957
- ...row.providerRef
38958
- });
39241
+ await stopWorkspaceProvider(store, driver, row, this.context.graceSeconds(row), at);
38959
39242
  } catch (err) {
38960
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;
38961
39252
  }
38962
39253
  }
38963
39254
  if (retainStorage) {
@@ -38997,16 +39288,12 @@ class SchedulerLifecycle {
38997
39288
  }
38998
39289
  });
38999
39290
  }
39000
- async stopAndRemove(ref, graceSeconds) {
39001
- await this.context.deps.driver.stop(ref, graceSeconds);
39002
- await this.context.deps.driver.remove(ref);
39003
- }
39004
39291
  async fail(row, reason, at) {
39005
39292
  const action = row.templateSnapshot.spec.persistence.checkpoint.onFailure;
39006
39293
  if (action === "preserve" && ["connected", "ready"].includes(row.state) && await this.requestPolicyPreserve(row, "failure")) {
39007
39294
  return;
39008
39295
  }
39009
- await this.finalize(row, "failed", reason, at, action === "retain-for-recovery");
39296
+ await this.finalize(row, "failed", reason, at);
39010
39297
  }
39011
39298
  async handleProcessExit(row, exitCode, at) {
39012
39299
  if (exitCode === 0 && row.templateSnapshot.spec.persistence.checkpoint.onCleanExit === "preserve" && await this.requestPolicyPreserve(row, "clean_exit")) {
@@ -39051,6 +39338,8 @@ class SchedulerSweep {
39051
39338
  for (const row of rows) {
39052
39339
  try {
39053
39340
  await this.sweepRow(row, now);
39341
+ if (row.providerRef)
39342
+ await this.context.deps.driver.inspect(row.providerRef);
39054
39343
  } catch (err) {
39055
39344
  this.context.report(`sweep.${row.id}`, err);
39056
39345
  }
@@ -39098,9 +39387,12 @@ class SchedulerSweep {
39098
39387
  }
39099
39388
  async sweepTerminating(row, now) {
39100
39389
  const stuckMs = 4 * this.context.timeoutMs(row, "terminateGrace") + 5000;
39101
- 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)
39102
39393
  return;
39103
- 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);
39104
39396
  }
39105
39397
  async sweepRow(row, now) {
39106
39398
  switch (row.state) {
@@ -39111,8 +39403,8 @@ class SchedulerSweep {
39111
39403
  if (row.registrationExpiresAt && now >= row.registrationExpiresAt) {
39112
39404
  if (row.provisioningMode === "warm" && row.launchAttempts < this.context.deps.limits.maxLaunchAttempts) {
39113
39405
  if (row.providerRef) {
39114
- await this.context.deps.driver.stop(row.providerRef, 1).catch(() => {});
39115
- 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);
39116
39408
  }
39117
39409
  await this.context.deps.store.transition(row.id, {
39118
39410
  from: ["provisioning"],
@@ -39170,6 +39462,9 @@ class Scheduler {
39170
39462
  });
39171
39463
  return this.context.activeTick;
39172
39464
  }
39465
+ async drain() {
39466
+ await this.context.activeTick;
39467
+ }
39173
39468
  async runTick() {
39174
39469
  await this.sweeper.sweep();
39175
39470
  await this.admission.admit();
@@ -39465,6 +39760,8 @@ async function appendTransition(context, tx, row, from, reason, at) {
39465
39760
  reasonCode: reason,
39466
39761
  occurredAt: at
39467
39762
  });
39763
+ if (row.purgeRequestedAt)
39764
+ return;
39468
39765
  const payload = buildEventEnvelope(row, at);
39469
39766
  await tx.insert(context.tables.eventOutbox).values({
39470
39767
  id: payload.id,
@@ -39509,7 +39806,7 @@ function createWarmPools(context) {
39509
39806
  async claimWarmPoolRuntime(claim) {
39510
39807
  const result = await db.transaction(async (tx) => {
39511
39808
  const [current] = await tx.select().from(workspaces3).where(eq(workspaces3.id, claim.workspaceId)).for("update");
39512
- if (current?.state !== "queued")
39809
+ if (current?.state !== "queued" || current.purgeRequestedAt)
39513
39810
  return null;
39514
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 });
39515
39812
  if (!runtime)
@@ -39575,7 +39872,7 @@ function createAdmission(context) {
39575
39872
  const workspace = await db.transaction(async (tx) => {
39576
39873
  await lock(tx, `${schema}:workspace-admission`, 7351);
39577
39874
  const [current] = await tx.select().from(workspaces3).where(eq(workspaces3.id, claim.workspaceId)).for("update");
39578
- if (current?.state !== "queued")
39875
+ if (current?.state !== "queued" || current.purgeRequestedAt)
39579
39876
  return null;
39580
39877
  const counts = await countActive(tx);
39581
39878
  if (counts.global >= claim.limits.globalActiveWorkspaces)
@@ -39664,7 +39961,7 @@ function createWorkspaces(context) {
39664
39961
  const cursor = db.select({ createdAt: workspaces3.createdAt, id: workspaces3.id }).from(workspaces3).where(and(eq(workspaces3.id, filter.cursor), eq(workspaces3.principalId, principalId)));
39665
39962
  cursorFilter = sql`(${workspaces3.createdAt}, ${workspaces3.id}) < (${cursor})`;
39666
39963
  }
39667
- const rows = await db.select().from(workspaces3).where(and(eq(workspaces3.principalId, principalId), filter.externalId ? eq(workspaces3.externalId, filter.externalId) : undefined, filter.state ? eq(workspaces3.state, filter.state) : undefined, filter.template ? eq(workspaces3.templateName, filter.template) : undefined, filter.metadata ? sql`${workspaces3.metadata} @> ${sql.param(filter.metadata, workspaces3.metadata)}::jsonb` : undefined, filter.createdAfter ? gte(workspaces3.createdAt, filter.createdAfter) : undefined, filter.createdBefore ? lt(workspaces3.createdAt, filter.createdBefore) : undefined, cursorFilter)).orderBy(desc2(workspaces3.createdAt), desc2(workspaces3.id)).limit(filter.limit);
39964
+ const rows = await db.select().from(workspaces3).where(and(eq(workspaces3.principalId, principalId), filter.externalId ? eq(workspaces3.externalId, filter.externalId) : undefined, filter.state ? eq(workspaces3.state, filter.state) : undefined, filter.template ? eq(workspaces3.templateName, filter.template) : undefined, filter.metadata ? sql`${structuredJsonValue(workspaces3.metadata)} @> ${sql.param(filter.metadata, workspaces3.metadata)}::jsonb` : undefined, filter.createdAfter ? gte(workspaces3.createdAt, filter.createdAfter) : undefined, filter.createdBefore ? lt(workspaces3.createdAt, filter.createdBefore) : undefined, cursorFilter)).orderBy(desc2(workspaces3.createdAt), desc2(workspaces3.id)).limit(filter.limit);
39668
39965
  return rows.map(workspaceFromRow);
39669
39966
  }
39670
39967
  };
@@ -39695,7 +39992,15 @@ function createTransitions(context) {
39695
39992
  return {
39696
39993
  async updateWorkspace(id, patch, at) {
39697
39994
  const bumpsChange = Object.keys(patch).some((key) => CHANGE_PATCH_KEYS.has(key));
39698
- 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
+ });
39699
40004
  if (bumpsChange)
39700
40005
  notifyChange(context, id);
39701
40006
  },
@@ -39704,9 +40009,12 @@ function createTransitions(context) {
39704
40009
  const [current] = await tx.select().from(workspaces3).where(eq(workspaces3.id, id)).for("update");
39705
40010
  if (!current || !req.from.includes(current.state) || !canTransition(current.state, req.to))
39706
40011
  return null;
40012
+ if (current.purgeRequestedAt && ["provisioning", "connected", "ready", "preserving", "queued"].includes(req.to))
40013
+ return null;
39707
40014
  const terminal = isTerminal(req.to);
39708
40015
  const [row] = await tx.update(workspaces3).set({
39709
40016
  ...req.patch,
40017
+ ...current.purgeRequestedAt ? purgedContentPatch() : {},
39710
40018
  state: req.to,
39711
40019
  reasonCode: req.reason,
39712
40020
  updatedAt: req.at,
@@ -39737,9 +40045,12 @@ function createTransitions(context) {
39737
40045
 
39738
40046
  // ../db/src/store.ts
39739
40047
  class PostgresStore {
39740
- constructor(databaseUrl, schema = "pocketcoder") {
39741
- const context = createDatabaseContext(databaseUrl, schema);
40048
+ constructor(databaseUrl, schema = "pocketcoder", options = {}) {
40049
+ const context = createDatabaseContext(databaseUrl, schema, options);
39742
40050
  const lifecycle2 = createLifecycle(context);
40051
+ const content = createContentPurge(context);
40052
+ this.listWorkspaceStorage = content.listWorkspaceStorage;
40053
+ this.purgeWorkspaceContent = content.purgeWorkspaceContent;
39743
40054
  this.init = lifecycle2.init;
39744
40055
  this.close = lifecycle2.close;
39745
40056
  this.acquireCoordinatorLease = lifecycle2.acquireCoordinatorLease;
@@ -39754,6 +40065,10 @@ class PostgresStore {
39754
40065
  this.listPrincipals = auth2.listPrincipals;
39755
40066
  this.updatePrincipal = auth2.updatePrincipal;
39756
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;
39757
40072
  this.insertMachineKey = auth2.insertMachineKey;
39758
40073
  this.getMachineKeyWithPrincipal = auth2.getMachineKeyWithPrincipal;
39759
40074
  this.revokeMachineKey = auth2.revokeMachineKey;
@@ -39826,6 +40141,8 @@ class PostgresStore {
39826
40141
  this.appendEvent = outbox2.appendEvent;
39827
40142
  this.waitForWorkspaceChange = createWorkspaceWaiter(context, workspaces3.getWorkspace);
39828
40143
  }
40144
+ listWorkspaceStorage;
40145
+ purgeWorkspaceContent;
39829
40146
  init;
39830
40147
  close;
39831
40148
  acquireCoordinatorLease;
@@ -39838,6 +40155,10 @@ class PostgresStore {
39838
40155
  listPrincipals;
39839
40156
  updatePrincipal;
39840
40157
  setPrincipalDisabled;
40158
+ getPrincipal;
40159
+ issueMachineKey;
40160
+ listMachineKeys;
40161
+ revokePrincipalKeys;
39841
40162
  insertMachineKey;
39842
40163
  getMachineKeyWithPrincipal;
39843
40164
  revokeMachineKey;
@@ -40163,6 +40484,51 @@ class OutputsApi extends WorkspaceCursorApi {
40163
40484
  }
40164
40485
  }
40165
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
+
40166
40532
  // ../sdk/src/resources/templates/templates.ts
40167
40533
  class TemplatesApi {
40168
40534
  transport;
@@ -40372,6 +40738,9 @@ class WorkspacesApi {
40372
40738
  preserve(id, input, key, options = {}) {
40373
40739
  return this.jsonOperation(`/v1/workspaces/${encodeURIComponent(id)}/preserve`, input, key, PreserveResponseSchema, options);
40374
40740
  }
40741
+ purge(id, key, options = {}) {
40742
+ return this.jsonOperation(`/v1/workspaces/${encodeURIComponent(id)}/purge`, {}, key, OperationResourceSchema, options);
40743
+ }
40375
40744
  recreate(id, input, key, options = {}) {
40376
40745
  return this.jsonOperation(`/v1/workspaces/${encodeURIComponent(id)}/recreate`, input, key, RestoreResponseSchema, options);
40377
40746
  }
@@ -40533,6 +40902,8 @@ class PocketCoderClient {
40533
40902
  outputs;
40534
40903
  administration;
40535
40904
  terminals;
40905
+ keys;
40906
+ recovery;
40536
40907
  constructor(config2, fetchImpl = fetch) {
40537
40908
  this.transport = new PocketCoderTransport(config2, fetchImpl);
40538
40909
  this.templates = new TemplatesApi(this.transport);
@@ -40547,6 +40918,8 @@ class PocketCoderClient {
40547
40918
  this.outputs = new OutputsApi(this.transport);
40548
40919
  this.administration = new AdministrationApi(this.transport);
40549
40920
  this.terminals = new TerminalsApi(this.transport);
40921
+ this.keys = new KeysApi(this.transport);
40922
+ this.recovery = new RecoveryApi(this.transport);
40550
40923
  }
40551
40924
  raw(path, init = {}) {
40552
40925
  return this.transport.raw(path, init);
@@ -40792,7 +41165,7 @@ async function api2(path, init = {}) {
40792
41165
  }
40793
41166
  async function withStore(fn) {
40794
41167
  const { url: url2, schema } = dbConfig();
40795
- const store = new PostgresStore(url2, schema);
41168
+ const store = new PostgresStore(url2, schema, { max: 1 });
40796
41169
  try {
40797
41170
  return await fn(store);
40798
41171
  } finally {
@@ -41051,66 +41424,103 @@ async function printFailureTail(workspaceId) {
41051
41424
  } catch {}
41052
41425
  }
41053
41426
 
41427
+ // src/commands/keys/issue.ts
41428
+ import { randomUUID as randomUUID11 } from "crypto";
41429
+
41054
41430
  // src/commands/scopes.ts
41055
41431
  function parseScopes(value) {
41056
- const items = valueList(value);
41057
- for (const item of items)
41432
+ return valueList(value).map((item) => {
41058
41433
  if (!isScope(item))
41059
41434
  fail(`unknown scope: ${item}`);
41060
- return items;
41435
+ return item;
41436
+ });
41061
41437
  }
41062
41438
 
41063
41439
  // src/commands/keys/issue.ts
41064
41440
  function addIssueCommand(parser2) {
41065
- return addAction(parser2, "issue", "Issue a machine key", (command2) => command2.option("principal", {
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", {
41066
41442
  type: "string",
41067
- demandOption: true,
41068
- description: "Principal name"
41069
- }).option("scopes", {
41070
- type: "string",
41071
- description: "Comma-separated scopes; defaults to the principal scopes"
41072
- }).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", {
41073
41445
  type: "string",
41074
41446
  default: "never",
41075
- 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;
41076
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
+ }
41077
41477
  const pepper = process.env.POCKETCODER_AUTH_PEPPER;
41078
41478
  if (!pepper)
41079
- fail("POCKETCODER_AUTH_PEPPER is required to issue keys");
41479
+ fail("POCKETCODER_AUTH_PEPPER is required to issue keys locally");
41080
41480
  await withStore(async (store) => {
41081
41481
  const name2 = need(flags, "principal");
41082
41482
  const principal = await store.getPrincipalByName(name2);
41083
41483
  if (!principal)
41084
41484
  fail(`unknown principal: ${name2}`);
41085
- const expiresRaw = typeof flags.expires === "string" ? flags.expires : "never";
41086
- const expiresAt = expiresRaw === "never" ? null : new Date(expiresRaw);
41087
- if (expiresAt && Number.isNaN(expiresAt.getTime())) {
41088
- fail(`invalid --expires value: ${expiresRaw}`);
41089
- }
41090
- const key = issueMachineKey(pepper);
41091
- await store.insertMachineKey({
41092
- id: key.id,
41093
- principalId: principal.id,
41094
- secretDigest: key.secretDigest,
41095
- scopes: typeof flags.scopes === "string" ? parseScopes(flags.scopes) : [],
41096
- createdAt: new Date,
41097
- expiresAt,
41098
- revokedAt: null,
41099
- lastUsedAt: null
41100
- });
41101
- console.log("machine key (shown once, store it now):");
41102
- 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
+ }
41103
41497
  });
41104
41498
  });
41105
41499
  }
41106
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
+
41107
41513
  // src/commands/keys/revoke.ts
41108
41514
  function addRevokeCommand(parser2) {
41109
- 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", {
41110
41516
  type: "string",
41111
41517
  demandOption: true,
41112
41518
  description: "Machine key ID"
41113
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
+ }
41114
41524
  await withStore(async (store) => {
41115
41525
  const id = need(flags, "id");
41116
41526
  const revoked = await store.revokeMachineKey(id, new Date);
@@ -41119,13 +41529,20 @@ function addRevokeCommand(parser2) {
41119
41529
  });
41120
41530
  }
41121
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
+
41122
41539
  // src/commands/keys/index.ts
41123
41540
  function addKeyCommands(parser2) {
41124
- 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));
41125
41542
  }
41126
41543
 
41127
41544
  // src/commands/pools/list.ts
41128
- function addListCommand2(parser2) {
41545
+ function addListCommand3(parser2) {
41129
41546
  return addAction(parser2, "list", "List warm pool inventory and metrics", (command2) => command2.option("json", { type: "boolean", description: "Print JSON" }), async (flags) => {
41130
41547
  const body = await controlPlaneClient().administration.warmPools();
41131
41548
  if (flags.json)
@@ -41142,7 +41559,7 @@ function addListCommand2(parser2) {
41142
41559
 
41143
41560
  // src/commands/pools/index.ts
41144
41561
  function addPoolCommands(parser2) {
41145
- return addResource(parser2, "pools", "Inspect operator-managed warm capacity", addListCommand2);
41562
+ return addResource(parser2, "pools", "Inspect operator-managed warm capacity", addListCommand3);
41146
41563
  }
41147
41564
 
41148
41565
  // src/commands/principals/options.ts
@@ -41168,7 +41585,7 @@ function addCreateCommand(parser2) {
41168
41585
  }
41169
41586
 
41170
41587
  // src/commands/principals/list.ts
41171
- function addListCommand3(parser2) {
41588
+ function addListCommand4(parser2) {
41172
41589
  return addAction(parser2, "list", "List principals", unchanged, async () => {
41173
41590
  await withStore(async (store) => {
41174
41591
  for (const principal of await store.listPrincipals()) {
@@ -41197,21 +41614,72 @@ function addUpdateCommand(parser2) {
41197
41614
 
41198
41615
  // src/commands/principals/index.ts
41199
41616
  function addPrincipalCommands(parser2) {
41200
- return addResource(parser2, "principals", "Manage principals", (commands) => addListCommand3(addUpdateCommand(addCreateCommand(commands))));
41617
+ return addResource(parser2, "principals", "Manage principals", (commands) => addListCommand4(addUpdateCommand(addCreateCommand(commands))));
41201
41618
  }
41202
41619
 
41203
41620
  // src/commands/server/process.ts
41204
41621
  import { spawn, spawnSync } from "child_process";
41205
- import { createHash as createHash4, randomUUID as randomUUID26 } from "crypto";
41206
- 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";
41207
41624
  import { homedir } from "os";
41208
41625
  import { dirname as dirname7, join as join8, resolve as resolve11 } from "path";
41209
41626
 
41210
41627
  // ../server/src/config/config.ts
41211
41628
  import { randomBytes as randomBytes2 } from "crypto";
41212
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
+
41213
41681
  // ../server/src/persistence/persistence-base.ts
41214
- import { randomUUID as randomUUID11 } from "crypto";
41682
+ import { randomUUID as randomUUID12 } from "crypto";
41215
41683
  var DEFAULT_PERSISTENCE_LIMITS = {
41216
41684
  maxRetainedBytes: 500 * 1024 ** 3,
41217
41685
  maxRetainedBytesPerPrincipal: 100 * 1024 ** 3,
@@ -41354,7 +41822,7 @@ class PersistenceContext {
41354
41822
  async emitCheckpointEvent(type, row) {
41355
41823
  const at = this.now();
41356
41824
  await this.deps.store.appendEvent(row.workspaceId, type, {
41357
- id: randomUUID11(),
41825
+ id: randomUUID12(),
41358
41826
  type,
41359
41827
  occurred_at: at.toISOString(),
41360
41828
  checkpoint: {
@@ -41370,7 +41838,7 @@ class PersistenceContext {
41370
41838
  }
41371
41839
  }
41372
41840
  // ../server/src/persistence/persistence-checkpoints.ts
41373
- import { randomUUID as randomUUID12 } from "crypto";
41841
+ import { randomUUID as randomUUID13 } from "crypto";
41374
41842
  class PersistenceCheckpointService {
41375
41843
  context;
41376
41844
  constructor(context) {
@@ -41389,7 +41857,7 @@ class PersistenceCheckpointService {
41389
41857
  }
41390
41858
  const now = this.context.now();
41391
41859
  const inserted = await this.context.insertOperation({
41392
- id: randomUUID12(),
41860
+ id: randomUUID13(),
41393
41861
  principalId: principal.id,
41394
41862
  kind: "verify",
41395
41863
  state: "running",
@@ -41468,7 +41936,7 @@ class PersistenceCheckpointService {
41468
41936
  });
41469
41937
  const at = this.context.now();
41470
41938
  await this.context.deps.store.appendEvent(workspaceId, "workspace.output_published", {
41471
- id: randomUUID12(),
41939
+ id: randomUUID13(),
41472
41940
  type: "workspace.output_published",
41473
41941
  occurred_at: at.toISOString(),
41474
41942
  workspace_id: workspaceId,
@@ -41479,7 +41947,44 @@ class PersistenceCheckpointService {
41479
41947
  }
41480
41948
 
41481
41949
  // ../server/src/persistence/persistence-maintenance.ts
41482
- import { randomUUID as randomUUID13 } from "crypto";
41950
+ import { randomUUID as randomUUID14 } from "crypto";
41951
+
41952
+ // ../server/src/persistence/persistence-cleanup.ts
41953
+ async function cleanupPreservedStorage(context, storageId) {
41954
+ const { store, driver } = context.deps;
41955
+ const storage = await store.getStorage(storageId);
41956
+ if (storage?.state !== "retained")
41957
+ return;
41958
+ if (storage.lastErrorCode && storage.lastErrorCode !== "storage_cleanup_failed")
41959
+ return;
41960
+ const workspace = await store.getWorkspace(storage.workspaceId);
41961
+ if (workspace?.state !== "preserved" || !workspace.terminalAt)
41962
+ return;
41963
+ const checkpoints = (await store.listCheckpoints(storage.principalId)).filter((checkpoint) => checkpoint.storageId === storage.id);
41964
+ if (checkpoints.length === 0 || checkpoints.some((checkpoint) => checkpoint.state !== "deleted"))
41965
+ return;
41966
+ const checkpointIds = new Set(checkpoints.map((checkpoint) => checkpoint.id));
41967
+ const operations = await store.listIncompleteOperations();
41968
+ if (operations.some((operation) => operation.workspaceId === workspace.id || operation.resultWorkspaceId === workspace.id || operation.checkpointId !== null && checkpointIds.has(operation.checkpointId)))
41969
+ return;
41970
+ try {
41971
+ if ((await driver.list()).some((provider) => provider.workspaceId === workspace.id))
41972
+ return;
41973
+ await context.storageDriver().deleteStorage(storage.providerRef);
41974
+ const now = context.now();
41975
+ await store.updateWorkspaceStorage(storage.id, {
41976
+ state: "deleted",
41977
+ deletedAt: now,
41978
+ retainedUntil: null,
41979
+ lastErrorCode: null
41980
+ }, now);
41981
+ } catch (error51) {
41982
+ await store.updateWorkspaceStorage(storage.id, { lastErrorCode: "storage_cleanup_failed" }, context.now());
41983
+ context.deps.log?.(`storage cleanup ${storage.id}: ${error51 instanceof Error ? error51.message : String(error51)}`);
41984
+ }
41985
+ }
41986
+
41987
+ // ../server/src/persistence/persistence-maintenance.ts
41483
41988
  class PersistenceMaintenanceService {
41484
41989
  context;
41485
41990
  constructor(context) {
@@ -41531,6 +42036,10 @@ class PersistenceMaintenanceService {
41531
42036
  this.context.deps.log?.(`retention ${checkpoint.id}: ${error51 instanceof Error ? error51.message : String(error51)}`);
41532
42037
  }
41533
42038
  }
42039
+ const deletedCheckpoints = await this.context.deps.store.listCheckpoints(principal.id, { state: "deleted" });
42040
+ for (const storageId of new Set(deletedCheckpoints.map((checkpoint) => checkpoint.storageId))) {
42041
+ await cleanupPreservedStorage(this.context, storageId);
42042
+ }
41534
42043
  }
41535
42044
  return { deleted, skipped, transcriptsDeleted };
41536
42045
  }
@@ -41557,8 +42066,14 @@ class PersistenceMaintenanceService {
41557
42066
  async delete(principal, checkpointId, idempotencyKey) {
41558
42067
  const requestDigest = digestOf({ checkpoint_id: checkpointId });
41559
42068
  const replay = await this.context.replayedOperation(principal.id, "delete", idempotencyKey, requestDigest, "Changed delete request.");
41560
- if (replay)
42069
+ if (replay) {
42070
+ if (replay.state === "succeeded") {
42071
+ const checkpoint2 = await this.context.deps.store.getCheckpoint(checkpointId);
42072
+ if (checkpoint2)
42073
+ await cleanupPreservedStorage(this.context, checkpoint2.storageId);
42074
+ }
41561
42075
  return replay;
42076
+ }
41562
42077
  const checkpoint = await this.context.getCheckpointOwned(principal, checkpointId);
41563
42078
  const activeRestore = (await this.context.deps.store.listIncompleteOperations()).some((operation) => operation.kind === "restore" && operation.checkpointId === checkpoint.id);
41564
42079
  if (activeRestore) {
@@ -41566,7 +42081,7 @@ class PersistenceMaintenanceService {
41566
42081
  }
41567
42082
  const now = this.context.now();
41568
42083
  const inserted = await this.context.insertOperation({
41569
- id: randomUUID13(),
42084
+ id: randomUUID14(),
41570
42085
  principalId: principal.id,
41571
42086
  kind: "delete",
41572
42087
  state: "running",
@@ -41602,12 +42117,13 @@ class PersistenceMaintenanceService {
41602
42117
  } catch {
41603
42118
  await this.context.failOperation(inserted.operation.id, "checkpoint_failed");
41604
42119
  }
42120
+ await cleanupPreservedStorage(this.context, checkpoint.storageId);
41605
42121
  return await this.context.deps.store.getOperation(inserted.operation.id) ?? inserted.operation;
41606
42122
  }
41607
42123
  }
41608
42124
 
41609
42125
  // ../server/src/persistence/persistence-preserve.ts
41610
- import { randomUUID as randomUUID14 } from "crypto";
42126
+ import { randomUUID as randomUUID15 } from "crypto";
41611
42127
  class PreservePersistenceService {
41612
42128
  context;
41613
42129
  preserveRunner;
@@ -41633,8 +42149,8 @@ class PreservePersistenceService {
41633
42149
  throw new ApiError("validation.invalid", "retention may not exceed the template checkpoint policy");
41634
42150
  }
41635
42151
  this.context.storageDriver();
41636
- const checkpointId = randomUUID14();
41637
- const operationId = randomUUID14();
42152
+ const checkpointId = randomUUID15();
42153
+ const operationId = randomUUID15();
41638
42154
  const now = this.context.now();
41639
42155
  const operationResult = await this.context.insertOperation({
41640
42156
  id: operationId,
@@ -41750,11 +42266,7 @@ class PersistencePreserveRunner {
41750
42266
  const quiesced = native || hook ? await this.context.deps.hub.prepareCheckpoint(workspace.id, operationId, deadlineMs) : false;
41751
42267
  if (!workspace.providerRef)
41752
42268
  return quiesced;
41753
- await this.context.deps.driver.stop({
41754
- kind: workspace.providerKind ?? "",
41755
- id: "",
41756
- ...workspace.providerRef
41757
- }, 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);
41758
42270
  return quiesced;
41759
42271
  }
41760
42272
  async createSnapshot(workspace, checkpoint, storage) {
@@ -41863,11 +42375,211 @@ class PersistencePreserveRunner {
41863
42375
  }
41864
42376
  }
41865
42377
 
41866
- // ../server/src/persistence/persistence-restore.ts
42378
+ // ../server/src/persistence/persistence-purge.ts
41867
42379
  import { randomUUID as randomUUID16 } from "crypto";
41868
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
+
41869
42581
  // ../server/src/workspaces/service.ts
41870
- import { randomUUID as randomUUID15 } from "crypto";
42582
+ import { randomUUID as randomUUID17 } from "crypto";
41871
42583
  function templateAuthorized(principal, templateName) {
41872
42584
  return principal.templateNames.includes("*") || principal.templateNames.includes(templateName);
41873
42585
  }
@@ -41982,7 +42694,7 @@ class WorkspaceService {
41982
42694
  this.validateCreateInput(template, body);
41983
42695
  const now = this.now();
41984
42696
  const result = await store.insertWorkspace({
41985
- id: randomUUID15(),
42697
+ id: randomUUID17(),
41986
42698
  principalId: principal.id,
41987
42699
  externalId: body.external_id,
41988
42700
  idempotencyKey,
@@ -42091,9 +42803,9 @@ class PersistenceRestoreService {
42091
42803
  }
42092
42804
  this.context.storageDriver();
42093
42805
  const now = this.context.now();
42094
- const resultWorkspaceId = randomUUID16();
42806
+ const resultWorkspaceId = randomUUID18();
42095
42807
  const operationResult = await this.context.insertOperation({
42096
- id: randomUUID16(),
42808
+ id: randomUUID18(),
42097
42809
  principalId: principal.id,
42098
42810
  kind: "restore",
42099
42811
  state: "pending",
@@ -42154,7 +42866,7 @@ class PersistenceRestoreService {
42154
42866
  }
42155
42867
  await this.context.deps.store.updateOperation(operationResult.operation.id, { resultWorkspaceId: inserted.workspace.id }, now);
42156
42868
  await this.context.deps.store.appendEvent(checkpoint.workspaceId, "workspace.restore_queued", {
42157
- id: randomUUID16(),
42869
+ id: randomUUID18(),
42158
42870
  type: "workspace.restore_queued",
42159
42871
  occurred_at: now.toISOString(),
42160
42872
  origin_workspace_id: checkpoint.workspaceId,
@@ -42173,6 +42885,9 @@ class PersistenceRestoreService {
42173
42885
  class PersistenceService {
42174
42886
  constructor(deps) {
42175
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);
42176
42891
  this.preserveRunner = new PersistencePreserveRunner(this.context);
42177
42892
  this.preservePersistence = new PreservePersistenceService(this.context, this.preserveRunner);
42178
42893
  this.checkpoint = new PersistenceCheckpointService(this.context);
@@ -42189,6 +42904,8 @@ class PersistenceService {
42189
42904
  this.delete = this.maintenance.delete.bind(this.maintenance);
42190
42905
  this.restore = this.restoreService.restore.bind(this.restoreService);
42191
42906
  }
42907
+ purge;
42908
+ retryPurges;
42192
42909
  restoreService;
42193
42910
  maintenance;
42194
42911
  checkpoint;
@@ -42539,6 +43256,10 @@ class DockerDriver {
42539
43256
  await rm(this.egressInputPath(workspaceId), { force: true });
42540
43257
  }
42541
43258
  }
43259
+ async purgeInput(workspaceId) {
43260
+ await rm(this.inputPath(workspaceId), { force: true });
43261
+ await rm(this.egressInputPath(workspaceId), { force: true });
43262
+ }
42542
43263
  async cleanupInput(workspaceId) {
42543
43264
  await rm(this.inputPath(workspaceId), { force: true });
42544
43265
  }
@@ -42834,7 +43555,7 @@ async function restoreCheckpointContent(checkpointRoot, target, manifest) {
42834
43555
  }
42835
43556
 
42836
43557
  // ../drivers/src/filesystem/filesystem-inventory.ts
42837
- import { open, readdir as readdir3, readFile, rm as rm3 } from "fs/promises";
43558
+ import { readdir as readdir3, readFile, rm as rm3 } from "fs/promises";
42838
43559
  import { join as join6, resolve as resolve8 } from "path";
42839
43560
  var STORAGE_METADATA_FILE = ".pocketcoder-storage.json";
42840
43561
  var CHECKPOINT_METADATA_FILE = ".pocketcoder-checkpoint.json";
@@ -42862,15 +43583,13 @@ async function discoverStorage(rootDirectory) {
42862
43583
  }
42863
43584
  async function discoverCheckpoints(rootDirectory) {
42864
43585
  const result = [];
42865
- for (const id of await readdir3(rootDirectory)) {
43586
+ for (const entry of await readdir3(rootDirectory)) {
43587
+ const id = entry.replace(/^\.creating-/, "");
42866
43588
  if (!/^[0-9a-f-]{36}$/i.test(id))
42867
43589
  continue;
42868
43590
  const root = childOf(rootDirectory, id);
42869
- try {
42870
- const handle = await open(join6(root, CHECKPOINT_METADATA_FILE), "r");
42871
- await handle.close();
43591
+ if (!result.some((row) => row.checkpointId === id))
42872
43592
  result.push({ checkpointId: id, ref: { kind: "filesystem", id, root } });
42873
- } catch {}
42874
43593
  }
42875
43594
  return result;
42876
43595
  }
@@ -43100,6 +43819,7 @@ class FilesystemStorageDriver {
43100
43819
  async deleteCheckpoint(ref) {
43101
43820
  const checkpoint = this.checkpointRef(ref);
43102
43821
  await deleteCheckpointRoot(checkpoint.root);
43822
+ await deleteCheckpointRoot(join7(this.checkpointRoot, `.creating-${checkpoint.id}`));
43103
43823
  }
43104
43824
  async listStorage() {
43105
43825
  await this.initRoots();
@@ -43194,6 +43914,232 @@ function discoveredWarmRuntimes(output, kind, namespace) {
43194
43914
  });
43195
43915
  }
43196
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
+
43197
44143
  // ../drivers/src/kubernetes/kubernetes-scheduling.ts
43198
44144
  function schedulingFields(options) {
43199
44145
  return {
@@ -43342,7 +44288,7 @@ function workspaceJobManifest(launch, name2, inputSecret, egressSecret, options)
43342
44288
  backoffLimit: 0,
43343
44289
  ttlSecondsAfterFinished: 3600,
43344
44290
  template: {
43345
- metadata: { labels, annotations },
44291
+ metadata: { labels, annotations, ...options.podFinalizers ? { finalizers: options.podFinalizers } : {} },
43346
44292
  spec: {
43347
44293
  restartPolicy: "Never",
43348
44294
  automountServiceAccountToken: false,
@@ -43409,7 +44355,7 @@ function warmJobManifest(launch, name2, inputSecret, egressSecret, options) {
43409
44355
  backoffLimit: 0,
43410
44356
  ttlSecondsAfterFinished: 3600,
43411
44357
  template: {
43412
- metadata: { labels, annotations },
44358
+ metadata: { labels, annotations, ...options.podFinalizers ? { finalizers: options.podFinalizers } : {} },
43413
44359
  spec: {
43414
44360
  restartPolicy: "Never",
43415
44361
  automountServiceAccountToken: false,
@@ -43459,6 +44405,22 @@ function warmJobManifest(launch, name2, inputSecret, egressSecret, options) {
43459
44405
  };
43460
44406
  }
43461
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
+
43462
44424
  // ../drivers/src/kubernetes/kubernetes.ts
43463
44425
  class KubernetesDriver {
43464
44426
  kind = "kubernetes";
@@ -43470,7 +44432,9 @@ class KubernetesDriver {
43470
44432
  imagePullPolicy;
43471
44433
  egress;
43472
44434
  sidecarsSupported = false;
44435
+ captureEvidence;
43473
44436
  constructor(options = {}) {
44437
+ this.captureEvidence = options.captureTerminationEvidence ?? false;
43474
44438
  this.namespace = options.namespace ?? "default";
43475
44439
  if (!isKubernetesName(this.namespace)) {
43476
44440
  throw new Error("namespace must be a Kubernetes resource name");
@@ -43538,6 +44502,7 @@ class KubernetesDriver {
43538
44502
  nodeSelector: this.nodeSelector,
43539
44503
  tolerations: this.tolerations,
43540
44504
  imagePullPolicy: this.imagePullPolicy,
44505
+ ...this.captureEvidence ? { podFinalizers: [EVIDENCE_FINALIZER] } : {},
43541
44506
  egressImage: this.egress.egressImage
43542
44507
  });
43543
44508
  try {
@@ -43591,6 +44556,7 @@ class KubernetesDriver {
43591
44556
  nodeSelector: this.nodeSelector,
43592
44557
  tolerations: this.tolerations,
43593
44558
  imagePullPolicy: this.imagePullPolicy,
44559
+ ...this.captureEvidence ? { podFinalizers: [EVIDENCE_FINALIZER] } : {},
43594
44560
  egressImage: this.egress.egressImage
43595
44561
  });
43596
44562
  try {
@@ -43613,38 +44579,36 @@ class KubernetesDriver {
43613
44579
  }
43614
44580
  }
43615
44581
  async inspect(ref) {
43616
- try {
43617
- const output = await kubectl(this.kubectlBin, this.namespace, ["get", "job", ref.id, "-o", "json"]);
43618
- const job = JSON.parse(output);
43619
- const running = (job.status?.active ?? 0) > 0;
43620
- const completedExitCode = job.status?.succeeded ? 0 : 1;
43621
- return {
43622
- exists: true,
43623
- running,
43624
- exitCode: running || !(job.status?.succeeded || job.status?.failed) ? null : completedExitCode
43625
- };
43626
- } 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)
43627
44591
  return { exists: false, running: false, exitCode: null };
43628
- }
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
+ };
43629
44605
  }
43630
44606
  async stop(ref, graceSeconds) {
43631
- await kubectl(this.kubectlBin, this.namespace, [
43632
- "patch",
43633
- "job",
43634
- ref.id,
43635
- "--type=merge",
43636
- "-p",
43637
- '{"spec":{"suspend":true}}'
43638
- ]).catch(() => {});
43639
- await kubectl(this.kubectlBin, this.namespace, [
43640
- "delete",
43641
- "pod",
43642
- "-l",
43643
- `job-name=${ref.id}`,
43644
- `--grace-period=${graceSeconds}`,
43645
- "--wait=true",
43646
- "--ignore-not-found"
43647
- ]).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);
43648
44612
  }
43649
44613
  async remove(ref) {
43650
44614
  await kubectl(this.kubectlBin, this.namespace, [
@@ -43652,19 +44616,31 @@ class KubernetesDriver {
43652
44616
  "job",
43653
44617
  ref.id,
43654
44618
  "--ignore-not-found",
44619
+ "--cascade=foreground",
43655
44620
  "--wait=true"
43656
- ]).catch(() => {});
44621
+ ]);
43657
44622
  const inputSecret = typeof ref.inputSecret === "string" ? ref.inputSecret : `${ref.id}-input`;
43658
- 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"]);
43659
44624
  if (typeof ref.egressSecret === "string") {
43660
- await kubectl(this.kubectlBin, this.namespace, [
43661
- "delete",
43662
- "secret",
43663
- ref.egressSecret,
43664
- "--ignore-not-found"
43665
- ]).catch(() => {});
44625
+ await kubectl(this.kubectlBin, this.namespace, ["delete", "secret", ref.egressSecret, "--ignore-not-found"]);
43666
44626
  }
43667
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
+ }
43668
44644
  async cleanupInput(workspaceId) {
43669
44645
  await kubectl(this.kubectlBin, this.namespace, [
43670
44646
  "delete",
@@ -44189,6 +45165,7 @@ function loadConfig(env2 = process.env) {
44189
45165
  }
44190
45166
  return {
44191
45167
  listenHost: env2.POCKETCODER_HOST ?? "127.0.0.1",
45168
+ launchPolicy: launchPolicyConfig(env2),
44192
45169
  listenPort,
44193
45170
  storeKind,
44194
45171
  databaseUrl,
@@ -44217,8 +45194,7 @@ function loadConfig(env2 = process.env) {
44217
45194
  }
44218
45195
 
44219
45196
  // ../memory-store/src/modules/auth/memory-store-auth.ts
44220
- import { randomUUID as randomUUID17 } from "crypto";
44221
-
45197
+ import { randomUUID as randomUUID19 } from "crypto";
44222
45198
  class MemoryAuthStore {
44223
45199
  context;
44224
45200
  constructor(context) {
@@ -44229,7 +45205,7 @@ class MemoryAuthStore {
44229
45205
  throw new Error(`principal exists: ${name2}`);
44230
45206
  }
44231
45207
  const row = {
44232
- id: randomUUID17(),
45208
+ id: randomUUID19(),
44233
45209
  name: name2,
44234
45210
  scopes,
44235
45211
  templateNames,
@@ -44242,6 +45218,21 @@ class MemoryAuthStore {
44242
45218
  async getPrincipalByName(name2) {
44243
45219
  return this.context.principals.find((p) => p.name === name2) ?? null;
44244
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
+ }
44245
45236
  async listPrincipals() {
44246
45237
  return this.context.principals.map((p) => ({ ...p }));
44247
45238
  }
@@ -44260,7 +45251,32 @@ class MemoryAuthStore {
44260
45251
  }
44261
45252
  }
44262
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.");
44263
45278
  this.context.keys.push({ ...row });
45279
+ return { key: { ...row }, created: true, conflict: false };
44264
45280
  }
44265
45281
  async getMachineKeyWithPrincipal(keyId) {
44266
45282
  const key = this.context.keys.find((k) => k.id === keyId);
@@ -44286,7 +45302,7 @@ class MemoryAuthStore {
44286
45302
  }
44287
45303
 
44288
45304
  // ../memory-store/src/state/memory-store-base.ts
44289
- import { randomUUID as randomUUID18 } from "crypto";
45305
+ import { randomUUID as randomUUID20 } from "crypto";
44290
45306
  var ACTIVE_STATES = [
44291
45307
  "provisioning",
44292
45308
  "connected",
@@ -44360,7 +45376,7 @@ class MemoryState {
44360
45376
  }
44361
45377
  appendHistory(row, from, to, reason, at) {
44362
45378
  this.history.push({
44363
- id: randomUUID18(),
45379
+ id: randomUUID20(),
44364
45380
  workspaceId: row.id,
44365
45381
  fromState: from,
44366
45382
  toState: to,
@@ -44369,6 +45385,8 @@ class MemoryState {
44369
45385
  });
44370
45386
  }
44371
45387
  appendWorkspaceEvent(row, at) {
45388
+ if (row.purgeRequestedAt)
45389
+ return;
44372
45390
  const payload = buildEventEnvelope(row, at);
44373
45391
  this.outbox.push({
44374
45392
  id: payload.id,
@@ -44394,6 +45412,8 @@ class MemoryConversationStore {
44394
45412
  const state = this.context.conversationStates.get(input.workspaceId);
44395
45413
  if (state?.status === "deleted")
44396
45414
  throw new Error("conversation.deleted");
45415
+ if (this.context.workspaces.get(input.workspaceId)?.purgeRequestedAt)
45416
+ throw new Error("conversation.deleted");
44397
45417
  const rows = this.context.conversations.get(input.workspaceId) ?? [];
44398
45418
  const existing = rows.find((row) => row.messageId === input.messageId);
44399
45419
  if (existing)
@@ -44469,6 +45489,8 @@ class MemoryLogStore {
44469
45489
  this.context = context;
44470
45490
  }
44471
45491
  async appendLogs(workspaceId, entries) {
45492
+ if (this.context.workspaces.get(workspaceId)?.purgeRequestedAt)
45493
+ return;
44472
45494
  const list = this.context.logs.get(workspaceId) ?? [];
44473
45495
  let bytes = this.context.logBytes.get(workspaceId) ?? 0;
44474
45496
  let seq = list.length > 0 ? list[list.length - 1]?.seq ?? 0 : 0;
@@ -44505,6 +45527,8 @@ class MemoryLogStore {
44505
45527
  };
44506
45528
  }
44507
45529
  async appendNetworkEvents(workspaceId, sourceSessionId, events) {
45530
+ if (this.context.workspaces.get(workspaceId)?.purgeRequestedAt)
45531
+ return;
44508
45532
  const rows = this.context.networkEvents.get(workspaceId) ?? [];
44509
45533
  const seen = new Set(rows.map((row) => `${row.sourceSessionId}:${row.source_seq}`));
44510
45534
  const workspace = this.context.workspaces.get(workspaceId);
@@ -44526,7 +45550,7 @@ class MemoryLogStore {
44526
45550
  }
44527
45551
 
44528
45552
  // ../memory-store/src/modules/outbox/memory-store-outbox.ts
44529
- import { randomUUID as randomUUID19 } from "crypto";
45553
+ import { randomUUID as randomUUID21 } from "crypto";
44530
45554
 
44531
45555
  class MemoryOutboxStore {
44532
45556
  context;
@@ -44557,8 +45581,10 @@ class MemoryOutboxStore {
44557
45581
  this.context.claimedEvents.delete(id);
44558
45582
  }
44559
45583
  async appendEvent(workspaceId, eventType, payload, at) {
45584
+ if (this.context.workspaces.get(workspaceId)?.purgeRequestedAt)
45585
+ return;
44560
45586
  this.context.outbox.push({
44561
- id: randomUUID19(),
45587
+ id: randomUUID21(),
44562
45588
  workspaceId,
44563
45589
  eventType,
44564
45590
  payload,
@@ -44571,6 +45597,52 @@ class MemoryOutboxStore {
44571
45597
  }
44572
45598
  }
44573
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
+
44574
45646
  // ../memory-store/src/modules/persistence/memory-store-operations.ts
44575
45647
  class MemoryOperationStore {
44576
45648
  context;
@@ -44590,6 +45662,12 @@ class MemoryOperationStore {
44590
45662
  throw new OperationCapacityExceededError;
44591
45663
  }
44592
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;
44593
45671
  this.context.operations.set(row.id, { ...row });
44594
45672
  return { operation: { ...row }, created: true, conflict: false };
44595
45673
  }
@@ -44612,12 +45690,12 @@ class MemoryOperationStore {
44612
45690
  async listIncompleteOperations() {
44613
45691
  return [...this.context.operations.values()].filter((row) => row.state === "pending" || row.state === "running").map((row) => ({ ...row }));
44614
45692
  }
44615
- async updateOperation(id, patch, at) {
45693
+ async updateOperation(id, patch2, at) {
44616
45694
  const row = this.context.operations.get(id);
44617
45695
  if (!row)
44618
45696
  return;
44619
- this.assertOperationReferences({ ...row, ...patch });
44620
- Object.assign(row, patch);
45697
+ this.assertOperationReferences({ ...row, ...patch2 });
45698
+ Object.assign(row, patch2);
44621
45699
  row.updatedAt = at;
44622
45700
  }
44623
45701
  async checkpointUsage(principalId) {
@@ -44634,6 +45712,8 @@ class MemoryOperationStore {
44634
45712
  const workspace = this.context.workspaces.get(input.workspaceId);
44635
45713
  if (!workspace)
44636
45714
  throw new Error("workspace not found");
45715
+ if (workspace.purgeRequestedAt)
45716
+ throw new ApiError("operation.conflict", "Workspace content is being purged.");
44637
45717
  const list = this.context.outputs.get(input.workspaceId) ?? [];
44638
45718
  const row = { ...input, seq: list.length + 1 };
44639
45719
  list.push(row);
@@ -44668,11 +45748,11 @@ class MemoryPersistenceStore {
44668
45748
  const row = this.context.storage.get(id);
44669
45749
  return row ? { ...row } : null;
44670
45750
  }
44671
- async updateWorkspaceStorage(id, patch, at) {
45751
+ async updateWorkspaceStorage(id, patch2, at) {
44672
45752
  const row = this.context.storage.get(id);
44673
45753
  if (!row)
44674
45754
  return;
44675
- Object.assign(row, patch);
45755
+ Object.assign(row, patch2);
44676
45756
  row.updatedAt = at;
44677
45757
  }
44678
45758
  async insertCheckpoint(row) {
@@ -44689,24 +45769,24 @@ class MemoryPersistenceStore {
44689
45769
  async listCheckpoints(principalId, filter = {}) {
44690
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 }));
44691
45771
  }
44692
- async updateCheckpoint(id, patch, at) {
45772
+ async updateCheckpoint(id, patch2, at) {
44693
45773
  const row = this.context.checkpoints.get(id);
44694
45774
  if (!row)
44695
45775
  return;
44696
45776
  if (row.state === "ready") {
44697
45777
  const mutable = new Set(["state", "reasonCode", "expiresAt", "deletedAt"]);
44698
- for (const key of Object.keys(patch)) {
45778
+ for (const key of Object.keys(patch2)) {
44699
45779
  if (!mutable.has(key))
44700
45780
  throw new Error("ready checkpoints are immutable");
44701
45781
  }
44702
45782
  }
44703
- Object.assign(row, patch);
45783
+ Object.assign(row, patch2);
44704
45784
  row.updatedAt = at;
44705
45785
  }
44706
45786
  }
44707
45787
 
44708
45788
  // ../memory-store/src/modules/templates/memory-store-templates.ts
44709
- import { randomUUID as randomUUID20 } from "crypto";
45789
+ import { randomUUID as randomUUID22 } from "crypto";
44710
45790
 
44711
45791
  class MemoryTemplateStore {
44712
45792
  context;
@@ -44722,7 +45802,7 @@ class MemoryTemplateStore {
44722
45802
  return { row: existing, created: false, conflict: false };
44723
45803
  }
44724
45804
  const row = {
44725
- id: randomUUID20(),
45805
+ id: randomUUID22(),
44726
45806
  name: input.name,
44727
45807
  version: input.version,
44728
45808
  digest: input.digest,
@@ -44828,16 +45908,16 @@ class MemoryWarmPoolStore {
44828
45908
  async listWarmPoolRuntimes() {
44829
45909
  return [...this.context.warmPoolRuntimes.values()].map((row) => ({ ...row }));
44830
45910
  }
44831
- async updateWarmPoolRuntime(id, patch, at) {
45911
+ async updateWarmPoolRuntime(id, patch2, at) {
44832
45912
  const row = this.context.warmPoolRuntimes.get(id);
44833
45913
  if (!row)
44834
45914
  return;
44835
- Object.assign(row, patch);
45915
+ Object.assign(row, patch2);
44836
45916
  row.updatedAt = at;
44837
45917
  }
44838
45918
  async claimWarmPoolRuntime(claim) {
44839
45919
  const workspace = this.context.workspaces.get(claim.workspaceId);
44840
- if (workspace?.state !== "queued")
45920
+ if (workspace?.state !== "queued" || workspace.purgeRequestedAt)
44841
45921
  return null;
44842
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];
44843
45923
  if (!runtime?.providerRef)
@@ -44899,7 +45979,7 @@ class MemoryAdmissionStore {
44899
45979
  }
44900
45980
  async claimWorkspaceAdmission(claim) {
44901
45981
  const workspace = this.context.workspaces.get(claim.workspaceId);
44902
- if (workspace?.state !== "queued")
45982
+ if (workspace?.state !== "queued" || workspace.purgeRequestedAt)
44903
45983
  return null;
44904
45984
  const counts = this.activeCounts();
44905
45985
  if (counts.global >= claim.limits.globalActiveWorkspaces)
@@ -44922,12 +46002,14 @@ class MemoryAdmissionStore {
44922
46002
  }
44923
46003
  });
44924
46004
  }
44925
- async updateWorkspace(id, patch, at) {
46005
+ async updateWorkspace(id, patch2, at) {
44926
46006
  const row = this.context.workspaces.get(id);
44927
46007
  if (!row)
44928
46008
  return;
44929
- Object.assign(row, patch);
44930
- 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));
44931
46013
  if (changed)
44932
46014
  row.changeSeq += 1;
44933
46015
  row.updatedAt = at;
@@ -44976,12 +46058,16 @@ class MemoryAdmissionStore {
44976
46058
  return null;
44977
46059
  if (!canTransition(row.state, req.to))
44978
46060
  return null;
46061
+ if (row.purgeRequestedAt && ["provisioning", "connected", "ready", "preserving", "queued"].includes(req.to))
46062
+ return null;
44979
46063
  const fromState = row.state;
44980
46064
  row.state = req.to;
44981
46065
  if (req.reason !== undefined)
44982
46066
  row.reasonCode = req.reason;
44983
46067
  if (req.patch)
44984
46068
  Object.assign(row, req.patch);
46069
+ if (row.purgeRequestedAt)
46070
+ Object.assign(row, purgedContentPatch());
44985
46071
  row.changeSeq += 1;
44986
46072
  row.updatedAt = req.at;
44987
46073
  if (isTerminal(req.to)) {
@@ -45078,6 +46164,7 @@ class MemoryWorkspaceStore {
45078
46164
  createdAt: row.createdAt,
45079
46165
  updatedAt: row.createdAt,
45080
46166
  terminalAt: null,
46167
+ purgeRequestedAt: null,
45081
46168
  originWorkspaceId: row.originWorkspaceId ?? null,
45082
46169
  restoredFromCheckpointId: row.restoredFromCheckpointId ?? null,
45083
46170
  sourceDescriptor: row.sourceDescriptor ?? null,
@@ -45130,6 +46217,9 @@ class MemoryWorkspaceStore {
45130
46217
  class MemoryStore {
45131
46218
  constructor() {
45132
46219
  this.context = new MemoryState;
46220
+ const content = createContentPurge2(this.context);
46221
+ this.listWorkspaceStorage = content.listWorkspaceStorage;
46222
+ this.purgeWorkspaceContent = content.purgeWorkspaceContent;
45133
46223
  this.persistence = new MemoryPersistenceStore(this.context);
45134
46224
  this.operation = new MemoryOperationStore(this.context);
45135
46225
  this.template = new MemoryTemplateStore(this.context);
@@ -45174,6 +46264,10 @@ class MemoryStore {
45174
46264
  this.listPrincipals = this.auth.listPrincipals.bind(this.auth);
45175
46265
  this.updatePrincipal = this.auth.updatePrincipal.bind(this.auth);
45176
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);
45177
46271
  this.insertMachineKey = this.auth.insertMachineKey.bind(this.auth);
45178
46272
  this.getMachineKeyWithPrincipal = this.auth.getMachineKeyWithPrincipal.bind(this.auth);
45179
46273
  this.revokeMachineKey = this.auth.revokeMachineKey.bind(this.auth);
@@ -45224,6 +46318,8 @@ class MemoryStore {
45224
46318
  operation;
45225
46319
  persistence;
45226
46320
  context;
46321
+ listWorkspaceStorage;
46322
+ purgeWorkspaceContent;
45227
46323
  init;
45228
46324
  acquireCoordinatorLease;
45229
46325
  close;
@@ -45257,6 +46353,10 @@ class MemoryStore {
45257
46353
  listPrincipals;
45258
46354
  updatePrincipal;
45259
46355
  setPrincipalDisabled;
46356
+ getPrincipal;
46357
+ issueMachineKey;
46358
+ listMachineKeys;
46359
+ revokePrincipalKeys;
45260
46360
  insertMachineKey;
45261
46361
  getMachineKeyWithPrincipal;
45262
46362
  revokeMachineKey;
@@ -45504,10 +46604,10 @@ function mapValues(object2, mapper) {
45504
46604
  });
45505
46605
  return result;
45506
46606
  }
45507
- function omit2(object2, keys) {
46607
+ function omit2(object2, keys2) {
45508
46608
  const result = {};
45509
46609
  Object.entries(object2).forEach(([key, value]) => {
45510
- if (!keys.some((keyToOmit) => keyToOmit === key)) {
46610
+ if (!keys2.some((keyToOmit) => keyToOmit === key)) {
45511
46611
  result[key] = value;
45512
46612
  }
45513
46613
  });
@@ -45882,8 +46982,8 @@ class DiscriminatedUnionTransformer {
45882
46982
  const refId = Metadata.getRefId(obj);
45883
46983
  const value = (_a4 = obj.def.shape) === null || _a4 === undefined ? undefined : _a4[discriminator];
45884
46984
  if (isZodType(value, "ZodEnum")) {
45885
- const keys = Object.values(value._zod.def.entries).filter(isString);
45886
- keys.forEach((enumValue) => {
46985
+ const keys2 = Object.values(value._zod.def.entries).filter(isString);
46986
+ keys2.forEach((enumValue) => {
45887
46987
  mapping[enumValue] = generateSchemaRef(refId);
45888
46988
  });
45889
46989
  return;
@@ -46010,8 +47110,8 @@ class RecordTransformer {
46010
47110
  const keyType = zodSchema.keyType;
46011
47111
  const propertiesSchema = isAnyZodType(propertiesType) ? mapItem(propertiesType) : {};
46012
47112
  if (isZodType(keyType, "ZodEnum")) {
46013
- const keys = Object.values(keyType._zod.def.entries).filter(isString);
46014
- 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 }), {});
46015
47115
  return Object.assign(Object.assign({}, mapNullableType("object")), { properties });
46016
47116
  }
46017
47117
  return Object.assign(Object.assign({}, mapNullableType("object")), { additionalProperties: propertiesSchema });
@@ -47313,9 +48413,9 @@ var handleParsingNestedValues = (form, key, value) => {
47313
48413
  return;
47314
48414
  }
47315
48415
  let nestedForm = form;
47316
- const keys = key.split(".");
47317
- keys.forEach((key2, index2) => {
47318
- if (index2 === keys.length - 1) {
48416
+ const keys2 = key.split(".");
48417
+ keys2.forEach((key2, index2) => {
48418
+ if (index2 === keys2.length - 1) {
47319
48419
  nestedForm[key2] = value;
47320
48420
  } else {
47321
48421
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
@@ -47351,8 +48451,8 @@ var HonoRequest = class {
47351
48451
  }
47352
48452
  #getAllDecodedParams() {
47353
48453
  const decoded = {};
47354
- const keys = Object.keys(this.#matchResult[0][this.routeIndex][1]);
47355
- for (const key of keys) {
48454
+ const keys2 = Object.keys(this.#matchResult[0][this.routeIndex][1]);
48455
+ for (const key of keys2) {
47356
48456
  const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]);
47357
48457
  if (value !== undefined) {
47358
48458
  decoded[key] = /\%/.test(value) ? tryDecodeURIComponent(value) : value;
@@ -48111,9 +49211,9 @@ function buildMatcherFromPreprocessedRoutes(routes) {
48111
49211
  if (!map2) {
48112
49212
  continue;
48113
49213
  }
48114
- const keys = Object.keys(map2);
48115
- for (let k = 0, len3 = keys.length;k < len3; k++) {
48116
- 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]]];
48117
49217
  }
48118
49218
  }
48119
49219
  }
@@ -48916,10 +50016,10 @@ var createBunWebSocket = () => ({
48916
50016
  });
48917
50017
 
48918
50018
  // ../server/src/http/middleware.ts
48919
- import { randomUUID as randomUUID21 } from "crypto";
50019
+ import { randomUUID as randomUUID23 } from "crypto";
48920
50020
  var requestId = async (c, next) => {
48921
50021
  const candidate = c.req.header("x-request-id");
48922
- 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();
48923
50023
  c.set("requestId", id);
48924
50024
  c.header("x-request-id", id);
48925
50025
  await next();
@@ -48966,6 +50066,7 @@ function machineAuth(store, pepper) {
48966
50066
  c.set("principal", found.principal);
48967
50067
  c.set("scopes", effectiveScopes);
48968
50068
  c.set("keyId", found.key.id);
50069
+ c.set("managedPrincipalIds", found.key.managedPrincipalIds);
48969
50070
  store.touchMachineKey(found.key.id, now).catch(() => {});
48970
50071
  await next();
48971
50072
  };
@@ -48980,7 +50081,7 @@ function requireScope(scope) {
48980
50081
  }
48981
50082
  function errorHandler2(logger) {
48982
50083
  return (err, c) => {
48983
- const id = c.get("requestId") ?? randomUUID21();
50084
+ const id = c.get("requestId") ?? randomUUID23();
48984
50085
  if (err instanceof ApiError) {
48985
50086
  return c.json(errorEnvelope(err.code, err.message, id, err.details), err.status);
48986
50087
  }
@@ -49082,8 +50183,219 @@ function registerAdministrationRoutes({
49082
50183
  });
49083
50184
  }
49084
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
+
49085
50397
  // ../server/src/attachments/attachments.ts
49086
- import { randomUUID as randomUUID22 } from "crypto";
50398
+ import { randomUUID as randomUUID24 } from "crypto";
49087
50399
  var ACK_TIMEOUT_MS = 30000;
49088
50400
  var RESULT_TIMEOUT_MS = 30000;
49089
50401
  var RESOLVE_TIMEOUT_MS = 1e4;
@@ -49224,6 +50536,8 @@ function attachmentUploadHandler(deps) {
49224
50536
  const id = c.req.param("id") ?? "";
49225
50537
  const attachmentId = (c.req.param("attachmentId") ?? "").toLowerCase();
49226
50538
  const row = await deps.service.getOwned(principal, id);
50539
+ if (row.purgeRequestedAt)
50540
+ throw new ApiError("operation.conflict", "Workspace content is being purged.");
49227
50541
  if (isTerminal(row.state)) {
49228
50542
  throw new ApiError("workspace.terminal", "This workspace has ended.");
49229
50543
  }
@@ -49235,7 +50549,7 @@ function attachmentUploadHandler(deps) {
49235
50549
  }
49236
50550
  const headers = uploadHeaders(c);
49237
50551
  const conn = liveConnection(deps.hub, id);
49238
- const operationId = randomUUID22();
50552
+ const operationId = randomUUID24();
49239
50553
  deps.hub.openAttachment(conn, operationId);
49240
50554
  try {
49241
50555
  const result = await streamUpload(deps.hub, conn, operationId, c.req.raw.body, headers, attachmentId);
@@ -49260,7 +50574,7 @@ function abandonUpload(hub, workspaceId, conn, operationId, error51) {
49260
50574
  hub.send(conn, "attachment_abort", { operation_id: operationId, reason });
49261
50575
  }
49262
50576
  async function resolveDescriptors(hub, conn, attachmentIds) {
49263
- const operationId = randomUUID22();
50577
+ const operationId = randomUUID24();
49264
50578
  hub.openAttachment(conn, operationId);
49265
50579
  try {
49266
50580
  hub.send(conn, "attachment_resolve", {
@@ -49314,7 +50628,7 @@ ${attachmentManifest(descriptors)}`;
49314
50628
  }
49315
50629
 
49316
50630
  // ../server/src/control-channel/hub.ts
49317
- import { randomUUID as randomUUID23 } from "crypto";
50631
+ import { randomUUID as randomUUID25 } from "crypto";
49318
50632
 
49319
50633
  // ../server/src/relay/relay-stream-channel.ts
49320
50634
  class RelayStreamRegistry {
@@ -49798,12 +51112,12 @@ class Hub {
49798
51112
  relay(workspaceId, request) {
49799
51113
  const conn = this.byWorkspace.get(workspaceId);
49800
51114
  if (!conn?.registered) {
49801
- return Promise.resolve({ request_id: randomUUID23(), headers: {}, error_code: "unreachable" });
51115
+ return Promise.resolve({ request_id: randomUUID25(), headers: {}, error_code: "unreachable" });
49802
51116
  }
49803
51117
  if (conn.inflight.size + conn.streams.size >= MAX_INFLIGHT_RELAY) {
49804
- return Promise.resolve({ request_id: randomUUID23(), headers: {}, error_code: "deadline" });
51118
+ return Promise.resolve({ request_id: randomUUID25(), headers: {}, error_code: "deadline" });
49805
51119
  }
49806
- const requestId2 = randomUUID23();
51120
+ const requestId2 = randomUUID25();
49807
51121
  return new Promise((resolve11) => {
49808
51122
  const timer = setTimeout(() => {
49809
51123
  conn.inflight.delete(requestId2);
@@ -49815,7 +51129,7 @@ class Hub {
49815
51129
  }
49816
51130
  relayStream(workspaceId, request, maxResponseBytes) {
49817
51131
  const conn = this.byWorkspace.get(workspaceId);
49818
- const requestId2 = randomUUID23();
51132
+ const requestId2 = randomUUID25();
49819
51133
  if (!conn?.registered) {
49820
51134
  return Promise.resolve({ request_id: requestId2, headers: {}, error_code: "unreachable" });
49821
51135
  }
@@ -50029,7 +51343,7 @@ async function authenticateConnection(deps, credentials) {
50029
51343
  return { error: "Missing workspace credentials." };
50030
51344
  }
50031
51345
  const row = await deps.store.getWorkspace(credentials.workspaceId);
50032
- if (!row || isTerminal(row.state))
51346
+ if (!row || row.purgeRequestedAt || isTerminal(row.state))
50033
51347
  return { error: "Unknown workspace." };
50034
51348
  if (sourceCredentialReference2(row) && credentials.protocolVersion < SOURCE_CREDENTIAL_MIN_PROTOCOL_VERSION) {
50035
51349
  return { error: "Source credentials require agent protocol version 6." };
@@ -50475,7 +51789,7 @@ function agentWsEvents(deps) {
50475
51789
  };
50476
51790
  }
50477
51791
  // ../server/src/conversations/conversations-routes.ts
50478
- import { randomUUID as randomUUID24 } from "crypto";
51792
+ import { randomUUID as randomUUID26 } from "crypto";
50479
51793
 
50480
51794
  // ../server/src/http/pagination.ts
50481
51795
  function encodeCursor(scope, value) {
@@ -50589,7 +51903,7 @@ function registerConversationRoutes({
50589
51903
  await store.deleteConversation(workspace.id, at);
50590
51904
  if (previous?.status !== "deleted") {
50591
51905
  await store.appendEvent(workspace.id, "workspace.conversation_deleted", {
50592
- id: randomUUID24(),
51906
+ id: randomUUID26(),
50593
51907
  type: "workspace.conversation_deleted",
50594
51908
  occurred_at: at.toISOString(),
50595
51909
  workspace_id: workspace.id
@@ -50787,7 +52101,9 @@ var HEALTHY_CHECKS = {
50787
52101
  database: "ok",
50788
52102
  schema: "ok",
50789
52103
  reconciliation: "ok",
50790
- coordinator: "ok"
52104
+ coordinator: "ok",
52105
+ "policy-reconciliation": "disabled",
52106
+ cleanup: "ok"
50791
52107
  };
50792
52108
 
50793
52109
  class Readiness {
@@ -51031,6 +52347,40 @@ function registerCheckpointRoutes(deps) {
51031
52347
  });
51032
52348
  }
51033
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
+
51034
52384
  // ../server/src/persistence/recovery-routes.ts
51035
52385
  function registerRecoveryRoutes({
51036
52386
  app,
@@ -51142,6 +52492,14 @@ function registerRecoveryRoutes({
51142
52492
 
51143
52493
  // ../server/src/relay/relay.ts
51144
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
+ }
51145
52503
  function requestPath(c, prefix) {
51146
52504
  const rawPath = c.req.path.startsWith(prefix) ? c.req.path.slice(prefix.length) : "";
51147
52505
  return rawPath === "" ? "/" : rawPath;
@@ -51240,12 +52598,7 @@ function relayHandler(deps, options) {
51240
52598
  const id = c.req.param("id") ?? "";
51241
52599
  const serviceName = options?.service ?? c.req.param("service") ?? "";
51242
52600
  const row = await deps.service.getOwned(principal, id);
51243
- if (isTerminal(row.state)) {
51244
- throw new ApiError("workspace.terminal", "This workspace has ended.");
51245
- }
51246
- if (row.state !== "ready") {
51247
- throw new ApiError("workspace.not_ready", "Workspace is not ready.");
51248
- }
52601
+ assertRelayReady(row);
51249
52602
  const prefix = options?.pathPrefix?.(id) ?? `/v1/workspaces/${id}/services/${serviceName}`;
51250
52603
  const path = requestPath(c, prefix);
51251
52604
  const method = c.req.method;
@@ -51257,6 +52610,8 @@ function relayHandler(deps, options) {
51257
52610
  let bodyB64 = await requestBody(c, method, match2.route.maxRequestBytes);
51258
52611
  if (options?.transformBodyB64)
51259
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.");
51260
52615
  if (!deps.hub.isConnected(id)) {
51261
52616
  throw new ApiError("workspace.disconnected", "The workspace supervisor is not currently connected.");
51262
52617
  }
@@ -51366,7 +52721,7 @@ function registerCatalogRoutes({ app, store }) {
51366
52721
  }
51367
52722
 
51368
52723
  // ../server/src/terminals/terminal-ws.ts
51369
- import { randomUUID as randomUUID25 } from "crypto";
52724
+ import { randomUUID as randomUUID27 } from "crypto";
51370
52725
  var SessionIdSchema = exports_external.uuid();
51371
52726
  function terminalConnectValidator(deps) {
51372
52727
  return async (c, next) => {
@@ -51379,6 +52734,8 @@ function terminalConnectValidator(deps) {
51379
52734
  async function terminalCapability(deps, c) {
51380
52735
  const workspaceId = c.req.param("id") ?? "";
51381
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.");
51382
52739
  if (isTerminal(workspace.state)) {
51383
52740
  throw new ApiError("workspace.terminal", "This workspace has ended.");
51384
52741
  }
@@ -51407,7 +52764,7 @@ async function terminalSession(deps, c, workspaceId, maxSessions) {
51407
52764
  if (c.req.header("upgrade")?.toLowerCase() !== "websocket") {
51408
52765
  throw new ApiError("validation.invalid", "WebSocket upgrade required.");
51409
52766
  }
51410
- const sessionId = parsedSessionId?.data ?? randomUUID25();
52767
+ const sessionId = parsedSessionId?.data ?? randomUUID27();
51411
52768
  if (requested) {
51412
52769
  const session2 = await deps.store.getTerminalSession(sessionId);
51413
52770
  if (!session2 || session2.workspaceId !== workspaceId) {
@@ -51710,6 +53067,7 @@ function buildServer(deps) {
51710
53067
  }) : undefined;
51711
53068
  const persistenceHolder = {};
51712
53069
  const scheduler = new Scheduler({
53070
+ ...deps.authorizeLaunch ? { authorizeLaunch: deps.authorizeLaunch } : {},
51713
53071
  store,
51714
53072
  driver,
51715
53073
  ...deps.storageDriver ? { storageDriver: deps.storageDriver } : {},
@@ -51810,9 +53168,12 @@ function buildServer(deps) {
51810
53168
  });
51811
53169
  app.use("/v1/*", machineAuth(store, pepper));
51812
53170
  registerCatalogRoutes({ app, store });
53171
+ registerKeyRoutes(app, store, pepper);
53172
+ registerOperatorRecoveryRoutes(app, store, persistence2);
51813
53173
  registerAdministrationRoutes({ app, persistence: persistence2, warmPool });
51814
53174
  registerCheckpointRoutes({ app, store, service, persistence: persistence2 });
51815
53175
  registerRecoveryRoutes({ app, store, service, persistence: persistence2 });
53176
+ registerPurgeRoutes(app, persistence2);
51816
53177
  registerConversationRoutes({ app, store, service });
51817
53178
  registerWorkspaceRoutes({ app, store, service });
51818
53179
  registerDiagnosticRoutes({ app, store, service });
@@ -51848,6 +53209,98 @@ function buildServer(deps) {
51848
53209
  };
51849
53210
  }
51850
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
+
51851
53304
  // ../server/src/lifecycle/lifecycle.ts
51852
53305
  var defaultLog = (message) => console.log(`[pocketcoder-server] ${message}`);
51853
53306
  async function initializeStore(config2, log) {
@@ -51885,6 +53338,7 @@ function createWorkspaceDriver(config2) {
51885
53338
  return new KubernetesDriver({
51886
53339
  ...egress,
51887
53340
  namespace: config2.kubernetesNamespace,
53341
+ captureTerminationEvidence: Boolean(config2.launchPolicy),
51888
53342
  nodeSelector: config2.kubernetesNodeSelector ?? undefined,
51889
53343
  tolerations: config2.kubernetesTolerations,
51890
53344
  ...config2.kubernetesServiceAccount ? { serviceAccountName: config2.kubernetesServiceAccount } : {}
@@ -51947,6 +53401,7 @@ function startExclusiveTimer(intervalMs, task, errorContext, log) {
51947
53401
  }
51948
53402
  async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
51949
53403
  const log = options.log ?? defaultLog;
53404
+ const authorizeLaunch = loadLaunchPolicy(config2.launchPolicy);
51950
53405
  const logger = createStructuredLogger((record3) => log(JSON.stringify(record3)));
51951
53406
  const metrics = new RuntimeMetrics;
51952
53407
  log(`config: ${JSON.stringify(configSummary(config2))}`);
@@ -51962,7 +53417,11 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
51962
53417
  const storageDriver = createStorageDriver(config2);
51963
53418
  const secretResolver = createSecretResolver(config2);
51964
53419
  const readiness = new Readiness({ reconciliation: "pending" }, metrics);
53420
+ const policyReconciliation = loadPolicyReconciliation(store, config2.launchPolicy);
53421
+ if (policyReconciliation)
53422
+ readiness.set("policy-reconciliation", "pending");
51965
53423
  const { app, websocket: websocket2, scheduler, persistence: persistence2, warmPool } = buildServer({
53424
+ ...authorizeLaunch ? { authorizeLaunch } : {},
51966
53425
  store,
51967
53426
  driver,
51968
53427
  ...storageDriver ? { storageDriver } : {},
@@ -51989,6 +53448,9 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
51989
53448
  const schedulerTimer = startExclusiveTimer(config2.schedulerIntervalMs, async () => {
51990
53449
  try {
51991
53450
  await scheduler.tick();
53451
+ const pendingPurges = await persistence2.retryPurges();
53452
+ readiness.set("cleanup", pendingPurges > 0 ? "pending" : "ok");
53453
+ metrics.observe("purge.pending", pendingPurges);
51992
53454
  readiness.set("coordinator", "ok");
51993
53455
  } catch (error51) {
51994
53456
  readiness.set("coordinator", "failed");
@@ -51996,6 +53458,15 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
51996
53458
  }
51997
53459
  }, "scheduler tick failed", log);
51998
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;
51999
53470
  const warmPoolTimer = warmPool && warmPoolContinuously ? startExclusiveTimer(config2.schedulerIntervalMs, () => warmPool.reconcile(), "warm pool reconciliation failed", log) : null;
52000
53471
  const retentionTimer = startExclusiveTimer(60000, async () => {
52001
53472
  const { deleted, skipped } = await persistence2.pruneExpired();
@@ -52016,6 +53487,9 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
52016
53487
  clearInterval(schedulerTimer);
52017
53488
  clearInterval(outboxTimer);
52018
53489
  clearInterval(retentionTimer);
53490
+ if (policyTimer)
53491
+ clearInterval(policyTimer);
53492
+ await policyReconciliation?.drain();
52019
53493
  if (warmPoolTimer)
52020
53494
  clearInterval(warmPoolTimer);
52021
53495
  throw error51;
@@ -52024,8 +53498,7 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
52024
53498
  const url2 = `http://${healthHost}:${server.port}`;
52025
53499
  log(`listening on http://${config2.listenHost}:${server.port}`);
52026
53500
  log(`workspaces reach this server at ${config2.workspaceServerUrl}`);
52027
- if (warmPool)
52028
- warmPool.reconcile().catch((error51) => log(`warm pool initial reconcile failed: ${String(error51)}`));
53501
+ const initialWarmPool = warmPool?.reconcile().catch((error51) => log(`warm pool initial reconcile failed: ${String(error51)}`));
52029
53502
  let stopPromise = null;
52030
53503
  return {
52031
53504
  config: config2,
@@ -52038,9 +53511,13 @@ async function startPocketCoderServer(config2 = loadConfig(), options = {}) {
52038
53511
  clearInterval(schedulerTimer);
52039
53512
  clearInterval(outboxTimer);
52040
53513
  clearInterval(retentionTimer);
53514
+ if (policyTimer)
53515
+ clearInterval(policyTimer);
53516
+ await policyReconciliation?.drain();
52041
53517
  if (warmPoolTimer)
52042
53518
  clearInterval(warmPoolTimer);
52043
53519
  await server.stop(true);
53520
+ await initialWarmPool;
52044
53521
  await store.close();
52045
53522
  })();
52046
53523
  return stopPromise;
@@ -52087,7 +53564,7 @@ function stateFromJson(value) {
52087
53564
  }
52088
53565
  function readState() {
52089
53566
  try {
52090
- return stateFromJson(JSON.parse(readFileSync4(statePath(), "utf8")));
53567
+ return stateFromJson(JSON.parse(readFileSync6(statePath(), "utf8")));
52091
53568
  } catch {
52092
53569
  return null;
52093
53570
  }
@@ -52095,7 +53572,7 @@ function readState() {
52095
53572
  function writeState(state) {
52096
53573
  const path = statePath();
52097
53574
  mkdirSync(dirname7(path), { recursive: true, mode: 448 });
52098
- const temporary = `${path}.${process.pid}.${randomUUID26()}.tmp`;
53575
+ const temporary = `${path}.${process.pid}.${randomUUID28()}.tmp`;
52099
53576
  writeFileSync(temporary, `${JSON.stringify(state, null, 2)}
52100
53577
  `, { mode: 384 });
52101
53578
  renameSync(temporary, path);
@@ -52167,7 +53644,7 @@ function selfInvocation(args) {
52167
53644
  }
52168
53645
  function logTail(path) {
52169
53646
  try {
52170
- const content = readFileSync4(path, "utf8");
53647
+ const content = readFileSync6(path, "utf8");
52171
53648
  return content.slice(Math.max(0, content.length - 4000)).trim();
52172
53649
  } catch {
52173
53650
  return "";
@@ -52204,7 +53681,7 @@ async function startManagedServer(options) {
52204
53681
  if (existing)
52205
53682
  removeState();
52206
53683
  const config2 = loadConfig();
52207
- const instanceToken = randomUUID26();
53684
+ const instanceToken = randomUUID28();
52208
53685
  const state = {
52209
53686
  version: 1,
52210
53687
  pid: 0,
@@ -52383,7 +53860,7 @@ function addStorageCommands(parser2) {
52383
53860
  }
52384
53861
 
52385
53862
  // src/commands/templates/list.ts
52386
- function addListCommand4(parser2) {
53863
+ function addListCommand5(parser2) {
52387
53864
  return addAction(parser2, "list", "List authorized template versions through the REST API", (command2) => command2.option("json", { type: "boolean", description: "Print JSON" }), async (flags) => {
52388
53865
  const items = await controlPlaneClient().templates.list();
52389
53866
  if (flags.json)
@@ -52476,7 +53953,7 @@ function addValidateCommand(parser2) {
52476
53953
 
52477
53954
  // src/commands/templates/index.ts
52478
53955
  function addTemplateCommands(parser2) {
52479
- 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)))));
52480
53957
  }
52481
53958
 
52482
53959
  // src/commands/workspaces/chat-session.ts
@@ -52508,8 +53985,8 @@ async function waitForAgentInput(id, options, deps) {
52508
53985
  }
52509
53986
 
52510
53987
  // src/commands/workspaces/attachments.ts
52511
- import { randomUUID as randomUUID27 } from "crypto";
52512
- 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";
52513
53990
  import { basename as basename2, extname as extname2 } from "path";
52514
53991
  var MEDIA_TYPES = {
52515
53992
  ".csv": "text/csv",
@@ -52541,12 +54018,12 @@ async function uploadAttachments(api3, workspaceId, paths, log) {
52541
54018
  const name2 = basename2(path);
52542
54019
  let bytes;
52543
54020
  try {
52544
- bytes = readFileSync5(path);
54021
+ bytes = readFileSync7(path);
52545
54022
  } catch {
52546
54023
  throw new Error(`could not read attachment file: ${path}`);
52547
54024
  }
52548
54025
  log(`uploading ${name2} (${bytes.byteLength} bytes)`);
52549
- const id = randomUUID27();
54026
+ const id = randomUUID29();
52550
54027
  const response = await api3(`/v1/workspaces/${workspaceId}/attachments/${id}`, {
52551
54028
  method: "PUT",
52552
54029
  headers: {
@@ -52614,7 +54091,7 @@ function fileFlags(value) {
52614
54091
  }
52615
54092
 
52616
54093
  // src/commands/workspaces/chat-cursor.ts
52617
- 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";
52618
54095
  import { homedir as homedir2 } from "os";
52619
54096
  import { dirname as dirname8, join as join10 } from "path";
52620
54097
  function cursorFile() {
@@ -52623,7 +54100,7 @@ function cursorFile() {
52623
54100
  }
52624
54101
  function readCursors(path) {
52625
54102
  try {
52626
- return JSON.parse(readFileSync6(path, "utf8"));
54103
+ return JSON.parse(readFileSync8(path, "utf8"));
52627
54104
  } catch {
52628
54105
  return {};
52629
54106
  }
@@ -52991,7 +54468,7 @@ function addChatCommand(parser2) {
52991
54468
  }
52992
54469
 
52993
54470
  // src/commands/workspaces/create.ts
52994
- import { randomUUID as randomUUID28 } from "crypto";
54471
+ import { randomUUID as randomUUID30 } from "crypto";
52995
54472
 
52996
54473
  // src/commands/workspaces/launch-input.ts
52997
54474
  function parseLaunchInput(value, fail2) {
@@ -53098,7 +54575,7 @@ async function waitForReady(initial, flags, { client, fail: failCommand }) {
53098
54575
  }
53099
54576
  async function createWorkspace(flags, deps) {
53100
54577
  const template = required2(flags, "template", deps.fail);
53101
- 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()}`;
53102
54579
  const launchInput = parseLaunchInput(flags.input, deps.fail);
53103
54580
  const created = await deps.client.workspaces.create({
53104
54581
  externalId,
@@ -53167,7 +54644,7 @@ async function listWorkspaces(flags) {
53167
54644
  else
53168
54645
  printWorkspaces(items);
53169
54646
  }
53170
- function addListCommand5(parser2) {
54647
+ function addListCommand6(parser2) {
53171
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);
53172
54649
  }
53173
54650
 
@@ -53210,14 +54687,29 @@ function addOutputsCommand(parser2) {
53210
54687
  }
53211
54688
 
53212
54689
  // src/commands/workspaces/preserve.ts
53213
- import { randomUUID as randomUUID29 } from "crypto";
54690
+ import { randomUUID as randomUUID31 } from "crypto";
53214
54691
  function addPreserveCommand(parser2) {
53215
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) => {
53216
54693
  const id = need(flags, "id");
53217
54694
  const result = await controlPlaneClient().workspaces.preserve(id, {
53218
54695
  ...typeof flags.retention === "string" ? { retention: flags.retention } : {},
53219
54696
  ...typeof flags.label === "string" ? { label: flags.label } : {}
53220
- }, `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);
53221
54713
  console.log(JSON.stringify(result, null, 2));
53222
54714
  });
53223
54715
  }
@@ -53392,7 +54884,7 @@ function addTerminalSessionsCommand(parser2) {
53392
54884
  // src/commands/workspaces/index.ts
53393
54885
  function addWorkspaceCommands(parser2) {
53394
54886
  const actions = [
53395
- addListCommand5,
54887
+ addListCommand6,
53396
54888
  addCreateCommand2,
53397
54889
  addGetCommand2,
53398
54890
  addLogsCommand,
@@ -53401,6 +54893,7 @@ function addWorkspaceCommands(parser2) {
53401
54893
  addTerminalCommand,
53402
54894
  addCancelCommand,
53403
54895
  addPreserveCommand,
54896
+ addPurgeCommand,
53404
54897
  addRestoreCommand,
53405
54898
  addRecreateCommand,
53406
54899
  addOutputsCommand,