@wtfalch/foundry 0.16.16 → 0.17.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.
Files changed (59) hide show
  1. package/dist/apply.d.ts +22 -1
  2. package/dist/bootstrap/bootstrap.d.ts +6 -1
  3. package/dist/bootstrap/bootstrap.js +26 -3
  4. package/dist/bootstrap/declare.d.ts +11 -1
  5. package/dist/bootstrap/declare.js +12 -2
  6. package/dist/index.d.ts +9 -4
  7. package/dist/index.js +7 -0
  8. package/dist/migrations/0038_calls_version_pin.sql +8 -0
  9. package/dist/migrations/0039_destroy_record.sql +7 -0
  10. package/dist/migrations/0040_company_included_uuid.sql +72 -0
  11. package/dist/migrations/0041_company_included_uuid_keys.sql +132 -0
  12. package/dist/migrations/0042_mutation_leases.sql +12 -0
  13. package/dist/org.d.ts +54 -0
  14. package/dist/org.js +43 -1
  15. package/dist/plan/adopt.d.ts +76 -0
  16. package/dist/plan/adopt.js +184 -0
  17. package/dist/plan/agora.d.ts +21 -0
  18. package/dist/plan/agora.js +37 -0
  19. package/dist/plan/broker-entries.d.ts +9 -1
  20. package/dist/plan/broker-entries.js +13 -5
  21. package/dist/plan/chat-matrix.d.ts +1 -1
  22. package/dist/plan/chat-matrix.js +11 -3
  23. package/dist/plan/core-files.d.ts +25 -0
  24. package/dist/plan/core-files.js +25 -0
  25. package/dist/plan/diff.d.ts +11 -3
  26. package/dist/plan/diff.js +27 -62
  27. package/dist/plan/entry-ai-key.d.ts +3 -3
  28. package/dist/plan/entry-files-key.d.ts +3 -3
  29. package/dist/plan/executor.d.ts +57 -23
  30. package/dist/plan/executor.js +134 -50
  31. package/dist/plan/index.d.ts +4 -0
  32. package/dist/plan/index.js +2 -0
  33. package/dist/plan/live.d.ts +5 -0
  34. package/dist/plan/login-host.d.ts +1 -1
  35. package/dist/plan/login-host.js +6 -5
  36. package/dist/plan/plan-apply.d.ts +139 -6
  37. package/dist/plan/plan-apply.js +546 -76
  38. package/dist/plan/platform-diff.d.ts +14 -0
  39. package/dist/plan/platform-diff.js +16 -1
  40. package/dist/plan/platform-org.d.ts +16 -0
  41. package/dist/plan/platform-org.js +17 -1
  42. package/dist/plan/platform-providers.d.ts +1 -1
  43. package/dist/plan/platform-providers.js +73 -11
  44. package/dist/plan/providers-readers.d.ts +2 -0
  45. package/dist/plan/providers-readers.js +4 -0
  46. package/dist/plan/rollout.d.ts +52 -0
  47. package/dist/plan/rollout.js +37 -0
  48. package/dist/plan/step-system.d.ts +22 -0
  49. package/dist/plan/step-system.js +37 -0
  50. package/dist/plan.d.ts +3 -0
  51. package/dist/state.d.ts +13 -0
  52. package/dist/store/audit.js +15 -1
  53. package/dist/store/postgres.d.ts +1 -1
  54. package/dist/store/postgres.js +127 -14
  55. package/dist/store/schema.d.ts +134 -5
  56. package/dist/store/schema.js +12 -1
  57. package/dist/store.d.ts +47 -4
  58. package/dist/store.js +9 -1
  59. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
- import type { PlanApply } from '../apply.js';
1
+ import type { DestroyProgress, PlanApply } from '../apply.js';
2
2
  import type { DeclaredOrg, OrgId } from '../org.js';
3
3
  import type { Plan, PlanStep } from '../plan.js';
4
4
  import type { DeclaredPlatform } from '../platform.js';
5
+ import type { JobRunner } from '../runner/types.js';
5
6
  import type { AppliedState } from '../state.js';
6
- import type { AppliedVersionsStore, DeclaredOrgStore, PlatformStore } from '../store.js';
7
+ import { type AppliedVersionsStore, type DeclaredOrgStore, type PlatformStore } from '../store.js';
8
+ import { type AdoptCandidate, type AdoptInput, type AdoptPlan } from './adopt.js';
7
9
  import { type AgoraHostPort } from './agora.js';
8
10
  import { type StepAction } from './diff.js';
9
11
  import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
@@ -50,6 +52,16 @@ export interface AuditLog {
50
52
  target?: 'platform';
51
53
  at: Date;
52
54
  }): Promise<void>;
55
+ /** `FoundryPlanApply.adopt` and `release` (archon#271): no `Plan` is applied, so this is its own entry. `entryKey` names the hosting entry. */
56
+ recordAdoption(entry: {
57
+ actor: AuditActor;
58
+ orgId: OrgId;
59
+ action: 'adopt' | 'release';
60
+ entryKey: string;
61
+ outcome: 'adopted' | 'released' | 'refused';
62
+ reason?: string;
63
+ at: Date;
64
+ }): Promise<void>;
53
65
  /** `FoundryPlanApply.destroy`'s own record -- no `Plan` involved, so this is its own entry shape rather than a reuse of `recordApply`'s. */
54
66
  recordDestroy(entry: {
55
67
  actor: AuditActor;
@@ -249,6 +261,13 @@ export interface StepDestroyer {
249
261
  readonly recordedIds: Readonly<Record<string, string>>;
250
262
  }): Promise<void>;
251
263
  }
264
+ /** #441: another apply, destroy, adopt or release holds the lease for this company or the platform. Nothing ran; try again when it finishes. */
265
+ export declare class MutationInProgressError extends Error {
266
+ readonly scope: string;
267
+ readonly heldUntil: Date;
268
+ /** @param heldUntil when the holder's lease runs out if it is never released. */
269
+ constructor(scope: string, heldUntil: Date);
270
+ }
252
271
  export declare class DeclaredOrgNotFoundError extends Error {
253
272
  constructor(orgId: OrgId);
254
273
  }
@@ -323,6 +342,27 @@ export interface FoundryPlanApplyOptions {
323
342
  aiOrganisation?: boolean;
324
343
  /** Overridable for tests; defaults to `() => new Date()`. */
325
344
  now?: () => Date;
345
+ /** #441: how long a mutation lease lasts before another caller may take it over. Defaults to 30 minutes. */
346
+ leaseTtlMs?: number;
347
+ /**
348
+ * #441 (part b): a durable, DB-row-backed record of `apply`, `destroy`,
349
+ * `adopt` and `release` for one org, held under the same mutation lease
350
+ * above rather than instead of it -- the lease is what actually keeps two
351
+ * holders from running `fn` at once (`withRunnerSerialization`'s own doc
352
+ * comment explains why the runner cannot safely take that job on its
353
+ * own). Optional, and a no-op without it -- every existing caller of this
354
+ * constructor is unaffected, and the lease alone still serializes every
355
+ * mutation the way it already did (#441, part a).
356
+ *
357
+ * What it adds: a row that never expires on a timer the way the lease
358
+ * does, so a crashed holder leaves something a host can see and query
359
+ * (`JobRunner.currentRun`/`getRun`) rather than nothing but a scope that
360
+ * quietly frees itself. Resuming after a crash reuses that row's id
361
+ * rather than starting a fresh one. Pass the same host-supplied
362
+ * `JobRunner` (`createJobRunner`, migrated with `runner/schema.ts`) that a
363
+ * host may also poll directly for progress.
364
+ */
365
+ runner?: JobRunner;
326
366
  /**
327
367
  * #253: appends a history row every time `runSteps` records a new
328
368
  * version as applied, beside the single-snapshot `store.putAppliedState`
@@ -379,10 +419,49 @@ export declare class FoundryPlanApply implements PlanApply {
379
419
  private readonly hostFiles;
380
420
  private readonly hostAi;
381
421
  private readonly now;
422
+ private readonly leaseTtlMs;
382
423
  private readonly appliedVersions;
383
424
  private readonly platformDeps;
425
+ private readonly runner;
384
426
  constructor(options: FoundryPlanApplyOptions);
385
427
  plan(id: OrgId): Promise<Plan>;
428
+ /**
429
+ * Archon#271: what adopting a live app and database into entry
430
+ * `input.entryKey` would do. A read, like {@link plan}: it reads the project
431
+ * through the reader and changes nothing. A mismatch is reported in the
432
+ * result, with no steps. Needs the apply permission, since it reads a
433
+ * project the caller names.
434
+ */
435
+ planAdopt(id: OrgId, input: AdoptInput): Promise<AdoptPlan>;
436
+ /**
437
+ * Archon#271: the live apps and databases in `projectUuid` this org has not
438
+ * recorded, for Changes' "Live, not theirs". A read, but it needs the apply
439
+ * permission: it reads a project the caller names.
440
+ */
441
+ adoptCandidates(id: OrgId, entryKey: string, projectUuid?: string): Promise<AdoptCandidate[]>;
442
+ /**
443
+ * Archon#271: records an app and database that already run in Coolify as
444
+ * entry `input.entryKey`'s resources, so the next plan finds them and plans
445
+ * no create. Verifies first and refuses on a mismatch or an entry that is
446
+ * already applied or held by another org. Same permission as {@link apply}.
447
+ * It never calls an executor: nothing in Coolify changes. Nothing redeploys
448
+ * here, but the next normal plan may propose updates to the adopted app
449
+ * that do (see `adopt.ts`); show that plan before applying it. Two
450
+ * concurrent adopts of one resource are not locked against each other.
451
+ */
452
+ adopt(id: OrgId, input: AdoptInput): Promise<AppliedState>;
453
+ /**
454
+ * Archon#271: the undo of {@link adopt}. Drops the entry's recorded Coolify
455
+ * ids and its `adopted.<entryKey>` snapshot (and `coolify_project` when
456
+ * adopt recorded it and no other Coolify id remains). It leaves the
457
+ * declaration alone: the entry goes back to declared and not applied, so the
458
+ * next plan creates it, exactly as before adopt. The host then adopts it
459
+ * again or removes the entry from the declaration. Deletes and stops nothing
460
+ * in Coolify. Only an adopted entry can be released. Same permission as
461
+ * {@link apply}. Every path writes an audit row.
462
+ */
463
+ release(id: OrgId, entryKey: string): Promise<AppliedState>;
464
+ private computeAdopt;
386
465
  apply(plan: Plan, confirmedOrgName: string): Promise<AppliedState>;
387
466
  /**
388
467
  * After an org apply: gives the platform files runner a changed share-domain
@@ -390,9 +469,10 @@ export declare class FoundryPlanApply implements PlanApply {
390
469
  * declared orgs) without a platform pass. Touches only the share hosts and
391
470
  * redeploys the runner; the platform files step itself never runs here.
392
471
  * Skips quietly for an actor who may not apply the platform, and when the
393
- * platform files step was never applied. There is no platform lock, so the
394
- * service ids are re-read just before the write and only the share-domains
395
- * key is recorded. A failure is rethrown naming that the org part succeeded.
472
+ * platform files step was never applied. It holds the platform lease for
473
+ * the refresh and fences its write with it; while another platform change
474
+ * holds the lease it skips (this class has no log), and the next platform
475
+ * apply does the refresh. Only the share-domains key is recorded. A failure is rethrown naming that the org part succeeded.
396
476
  */
397
477
  private refreshPlatformFilesShare;
398
478
  /**
@@ -421,7 +501,60 @@ export declare class FoundryPlanApply implements PlanApply {
421
501
  * declared org in place with whatever remains recorded in its applied
422
502
  * state, so a retried `destroy` only tears down what's left.
423
503
  */
424
- destroy(id: OrgId, confirmedOrgName: string): Promise<void>;
504
+ destroy(id: OrgId, confirmedOrgName: string, options?: {
505
+ onProgress?: (event: DestroyProgress) => void;
506
+ }): Promise<void>;
507
+ /** The steps `destroy` runs for `id`, in order. Refuses a non-test org like `destroy` does; writes no audit row since it changes nothing. */
508
+ planDestroy(id: OrgId): Promise<{
509
+ steps: string[];
510
+ kept: string[];
511
+ }>;
512
+ /**
513
+ * #441 (part b): records `fn`'s run in the durable runner, when one is
514
+ * configured, and calls it. Must run with the mutation lease already
515
+ * held -- every one of this method's four callers (`apply`, `destroy`,
516
+ * `adopt`, `release`) calls it from inside its own `withMutationLease`
517
+ * callback, never around it. The reason is `JobRunner.runSteps` itself:
518
+ * it checkpoints a run's one step (the literal id `'mutate'`, the same
519
+ * for all four callers so they share one row per org) by a plain read of
520
+ * the row followed by a later blind write, with nothing to stop two
521
+ * concurrent calls for the same `runId` from both reading "not yet
522
+ * checkpointed" and both calling `fn` -- the runner sequences and
523
+ * checkpoints (its own doc comment), it does not itself exclude a second
524
+ * caller the way the lease's atomic acquire does. Holding the lease first
525
+ * is what makes it safe to reuse an existing, not-yet-succeeded run
526
+ * ({@link JobRunner.currentRun}) rather than refuse it outright: by the
527
+ * time this method runs, this caller is already the lease's sole holder,
528
+ * so any row `currentRun` finds can only be a previous, now-abandoned
529
+ * attempt (the lease's own atomic acquire already refused a second live
530
+ * caller before either of them got here) -- reusing it is the resume #9
531
+ * calls "a human decides when to call it again", not a race.
532
+ *
533
+ * Two *first* attempts for the same org can still race the insert itself
534
+ * when neither has an existing row yet -- `JobRunner.enqueueApply`'s own
535
+ * unique constraint decides it at the database level, a real race rather
536
+ * than one decided by this process's await order, and the loser's
537
+ * {@link AlreadyRunningError} is refused and audited through `refuse`,
538
+ * the same as a lost lease. In practice this needs two lease holders at
539
+ * once to reach it, which the lease itself already prevents; it is this
540
+ * method's defence against a bug in the lease's own acquire path, not
541
+ * the primary lock.
542
+ *
543
+ * `fn`'s result is threaded out through a closure, since
544
+ * {@link RunnerStep.run} returns nothing.
545
+ */
546
+ private withRunnerSerialization;
547
+ /**
548
+ * #441: runs `fn` while holding the one mutation lease for `scope`, so two
549
+ * `FoundryPlanApply` instances on one store never run provider steps for the
550
+ * same company or platform at once. A second caller is refused with
551
+ * {@link MutationInProgressError}; `fn` gets the lease as the fence its
552
+ * store writes carry, so a worker that lost the lease cannot overwrite the
553
+ * newer holder's state. Both refusals are audited through `refuse`.
554
+ */
555
+ private withMutationLease;
556
+ /** #441: before each provider step, extends the lease; a holder that lost it stops here, before the step runs. */
557
+ private renewLease;
425
558
  private refuse;
426
559
  private refusePlatform;
427
560
  private refuseDestroy;