@cat-factory/integrations 0.118.1 → 0.120.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.
@@ -30,6 +30,14 @@ export interface CapabilityCredentialsServiceDependencies {
30
30
  * `BinaryGeneratorSource` rather than a registry anyway, which only the composition root can
31
31
  * supply.
32
32
  *
33
+ * CONCURRENCY: the row is ONE sealed blob holding the whole set, so the per-key writes
34
+ * ({@link put}, {@link remove}) are read-modify-write over it and ride a rev-guarded
35
+ * `compareAndSwap`: a blind upsert would let two operators saving DIFFERENT keys silently
36
+ * destroy each other's, with the loser's save still returning success. On a lost race the
37
+ * mutation reloads and re-applies on the winner's snapshot (it is a pure single-key edit, so
38
+ * re-application is trivially idempotent). The whole-set {@link set} stays a blind write on
39
+ * purpose: replacing whatever is stored IS its semantics.
40
+ *
33
41
  * {@link resolveValues} is the one place a value is decrypted, and its result reaches nothing but
34
42
  * the job body.
35
43
  */
@@ -47,17 +55,24 @@ export declare class CapabilityCredentialsService {
47
55
  *
48
56
  * A whole-set REPLACE rather than a merge, because the client cannot merge: it never received
49
57
  * the values, so a partial write would have to mean "leave the ones I did not send alone",
50
- * which is indistinguishable on the wire from "remove them". {@link remove} is the narrow
51
- * operation that would otherwise need that distinction.
58
+ * which is indistinguishable on the wire from "remove them". {@link put} and {@link remove}
59
+ * are the narrow operations that would otherwise need that distinction.
52
60
  */
53
61
  set(workspaceId: string, input: UpsertCapabilityCredentialsInput): Promise<CapabilityCredentialRef[]>;
54
62
  /**
55
- * Remove ONE stored credential, leaving the rest sealed as they are.
63
+ * Set ONE credential's value, leaving the rest sealed as they are. Replaces the value when the
64
+ * key is already stored, appends it otherwise.
56
65
  *
57
- * Re-seals the survivors rather than editing the blob in place, so this is a read-modify-write
58
- * and the last writer wins. That is the right trade for a form an operator edits: the
59
- * alternative is a row per key, and a lost update here costs one retyped secret where the N-row
60
- * shape costs every dispatch an N-row read.
66
+ * The narrow twin of {@link remove}, and the write a checklist UI actually performs: the
67
+ * client holds no values, so it can neither re-send the set nor express "leave the others
68
+ * alone" through {@link set}. Rev-guarded (see the class doc), so two operators saving
69
+ * different keys at the same moment both land.
70
+ */
71
+ put(workspaceId: string, key: string, value: string): Promise<CapabilityCredentialRef[]>;
72
+ /**
73
+ * Remove ONE stored credential, leaving the rest sealed as they are. Re-seals the survivors
74
+ * rather than editing the blob in place, rev-guarded like {@link put}; emptying the set
75
+ * deletes the row through the same guard, so a concurrent save's key cannot be swept away.
61
76
  */
62
77
  remove(workspaceId: string, key: string): Promise<CapabilityCredentialRef[]>;
63
78
  /**
@@ -66,7 +81,20 @@ export declare class CapabilityCredentialsService {
66
81
  * fallback (where a facade wired one) should answer instead.
67
82
  */
68
83
  resolveValues(workspaceId: string): Promise<CapabilityCredentialEntry[]>;
69
- private write;
84
+ /**
85
+ * Apply a pure single-key mutation under OPTIMISTIC CONCURRENCY: load, run `apply`, then
86
+ * `compareAndSwap` (or the rev-guarded delete when the set empties); on a lost race reload and
87
+ * re-apply on the winner's snapshot, bounded by {@link MAX_MUTATE_ATTEMPTS}.
88
+ *
89
+ * `apply` returns the next entry set, or null for "nothing to change" (a duplicate delete),
90
+ * which answers with the current summary untouched. `touchedKey` is the one key whose
91
+ * `updatedAt` this write may honestly stamp: "last set" is a per-key fact, so every OTHER
92
+ * entry keeps the timestamp the stored summary already carries. A write that re-stamped the
93
+ * whole set would falsify every neighbour's date.
94
+ */
95
+ private mutate;
96
+ /** Decrypt and validate the sealed entry blob; an absent row is an empty set. */
97
+ private decryptEntries;
70
98
  /** Parse the persisted non-secret summary, tolerating a corrupt row (the view still loads). */
71
99
  private parseSummary;
72
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CapabilityCredentialsService.d.ts","sourceRoot":"","sources":["../../../src/modules/capabilityCredentials/CapabilityCredentialsService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,8BAA8B,EAC9B,KAAK,EACL,YAAY,EACb,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EACV,yBAAyB,EACzB,uBAAuB,EACvB,gCAAgC,EACjC,MAAM,wBAAwB,CAAA;AAM/B;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,uCAAuC,CAAA;AAEtF,MAAM,WAAW,wCAAwC;IACvD,8BAA8B,EAAE,8BAA8B,CAAA;IAC9D,4FAA4F;IAC5F,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;CACb;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,4BAA4B;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,wCAAwC,EAAI;IAE/E;;;OAGG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAGxE;IAED;;;;;;;;OAQG;IACG,GAAG,CACP,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,gCAAgC,GACtC,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAMpC;IAED;;;;;;;OAOG;IACG,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAWjF;IAED;;;;OAIG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAM7E;YAEa,KAAK;IAiBnB,+FAA+F;IAC/F,OAAO,CAAC,YAAY;CAerB"}
1
+ {"version":3,"file":"CapabilityCredentialsService.d.ts","sourceRoot":"","sources":["../../../src/modules/capabilityCredentials/CapabilityCredentialsService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,8BAA8B,EAC9B,KAAK,EACL,YAAY,EACb,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EACV,yBAAyB,EACzB,uBAAuB,EACvB,gCAAgC,EACjC,MAAM,wBAAwB,CAAA;AAO/B;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,uCAAuC,CAAA;AAUtF,MAAM,WAAW,wCAAwC;IACvD,8BAA8B,EAAE,8BAA8B,CAAA;IAC9D,4FAA4F;IAC5F,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;CACb;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,4BAA4B;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,wCAAwC,EAAI;IAE/E;;;OAGG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAGxE;IAED;;;;;;;;OAQG;IACG,GAAG,CACP,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,gCAAgC,GACtC,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAoBpC;IAED;;;;;;;;OAQG;IACG,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAe7F;IAED;;;;OAIG;IACG,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAQjF;IAED;;;;OAIG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAE7E;IAED;;;;;;;;;;OAUG;YACW,MAAM;IA0CpB,iFAAiF;YACnE,cAAc;IAS5B,+FAA+F;IAC/F,OAAO,CAAC,YAAY;CAerB"}
@@ -1,10 +1,18 @@
1
- import { capabilityCredentialsSummary, parseCapabilityCredentialEntries, } from '@cat-factory/contracts';
1
+ import { ConflictError, ValidationError } from '@cat-factory/kernel';
2
+ import { MAX_CAPABILITY_CREDENTIALS, capabilityCredentialsSummary, parseCapabilityCredentialEntries, } from '@cat-factory/contracts';
2
3
  /**
3
4
  * HKDF domain tag separating the sealed capability-credential blob from every other cipher
4
5
  * (mirrors {@link TEST_SECRETS_CIPHER_INFO} et al). The facade builds a `WebCryptoSecretCipher`
5
6
  * keyed by `ENCRYPTION_KEY` with this info tag.
6
7
  */
7
8
  export const CAPABILITY_CREDENTIALS_CIPHER_INFO = 'cat-factory:capability-credentials';
9
+ /**
10
+ * How many times a contended per-key mutation reloads and re-applies before giving up with a
11
+ * 409. Matches `IterativeReviewService`'s budget: the contending writers here are human saves in
12
+ * two browser tabs, so a handful of retries covers real contention while still failing loudly on
13
+ * a pathological hot row.
14
+ */
15
+ const MAX_MUTATE_ATTEMPTS = 8;
8
16
  /**
9
17
  * Owns a workspace's capability credentials: the tenant-scoped, sealed home for the secrets a
10
18
  * tool server or a generative binary integration declares by name.
@@ -23,6 +31,14 @@ export const CAPABILITY_CREDENTIALS_CIPHER_INFO = 'cat-factory:capability-creden
23
31
  * `BinaryGeneratorSource` rather than a registry anyway, which only the composition root can
24
32
  * supply.
25
33
  *
34
+ * CONCURRENCY: the row is ONE sealed blob holding the whole set, so the per-key writes
35
+ * ({@link put}, {@link remove}) are read-modify-write over it and ride a rev-guarded
36
+ * `compareAndSwap`: a blind upsert would let two operators saving DIFFERENT keys silently
37
+ * destroy each other's, with the loser's save still returning success. On a lost race the
38
+ * mutation reloads and re-applies on the winner's snapshot (it is a pure single-key edit, so
39
+ * re-application is trivially idempotent). The whole-set {@link set} stays a blind write on
40
+ * purpose: replacing whatever is stored IS its semantics.
41
+ *
26
42
  * {@link resolveValues} is the one place a value is decrypted, and its result reaches nothing but
27
43
  * the job body.
28
44
  */
@@ -45,36 +61,68 @@ export class CapabilityCredentialsService {
45
61
  *
46
62
  * A whole-set REPLACE rather than a merge, because the client cannot merge: it never received
47
63
  * the values, so a partial write would have to mean "leave the ones I did not send alone",
48
- * which is indistinguishable on the wire from "remove them". {@link remove} is the narrow
49
- * operation that would otherwise need that distinction.
64
+ * which is indistinguishable on the wire from "remove them". {@link put} and {@link remove}
65
+ * are the narrow operations that would otherwise need that distinction.
50
66
  */
51
67
  async set(workspaceId, input) {
52
68
  if (input.entries.length === 0) {
53
69
  await this.deps.capabilityCredentialRepository.delete(workspaceId);
54
70
  return [];
55
71
  }
56
- return this.write(workspaceId, input.entries);
72
+ const now = this.deps.clock.now();
73
+ const existing = await this.deps.capabilityCredentialRepository.get(workspaceId);
74
+ // Every value in the set was just (re)typed, so every key is honestly stamped `now`.
75
+ const summary = capabilityCredentialsSummary(input.entries, now);
76
+ await this.deps.capabilityCredentialRepository.upsert({
77
+ workspaceId,
78
+ credentials: await this.deps.secretCipher.encrypt(JSON.stringify(input.entries)),
79
+ summary: JSON.stringify(summary),
80
+ // The insert value; a conflicting update bumps the STORED rev in SQL instead, so the rev
81
+ // stays monotonic under this blind write and a concurrent compareAndSwap reliably loses.
82
+ rev: 0,
83
+ createdAt: existing?.createdAt ?? now,
84
+ updatedAt: now,
85
+ });
86
+ return summary;
57
87
  }
58
88
  /**
59
- * Remove ONE stored credential, leaving the rest sealed as they are.
89
+ * Set ONE credential's value, leaving the rest sealed as they are. Replaces the value when the
90
+ * key is already stored, appends it otherwise.
60
91
  *
61
- * Re-seals the survivors rather than editing the blob in place, so this is a read-modify-write
62
- * and the last writer wins. That is the right trade for a form an operator edits: the
63
- * alternative is a row per key, and a lost update here costs one retyped secret where the N-row
64
- * shape costs every dispatch an N-row read.
92
+ * The narrow twin of {@link remove}, and the write a checklist UI actually performs: the
93
+ * client holds no values, so it can neither re-send the set nor express "leave the others
94
+ * alone" through {@link set}. Rev-guarded (see the class doc), so two operators saving
95
+ * different keys at the same moment both land.
96
+ */
97
+ async put(workspaceId, key, value) {
98
+ return this.mutate(workspaceId, key, (entries) => {
99
+ const existing = entries.findIndex((entry) => entry.key === key);
100
+ if (existing === -1 && entries.length >= MAX_CAPABILITY_CREDENTIALS) {
101
+ throw new ValidationError(`at most ${MAX_CAPABILITY_CREDENTIALS} capability credentials per workspace`, { reason: 'capability_credential_limit' });
102
+ }
103
+ const next = [...entries];
104
+ // Replace IN PLACE for a known key, so re-typing a value does not reorder the stored set.
105
+ if (existing === -1)
106
+ next.push({ key, value });
107
+ else
108
+ next[existing] = { key, value };
109
+ return next;
110
+ });
111
+ }
112
+ /**
113
+ * Remove ONE stored credential, leaving the rest sealed as they are. Re-seals the survivors
114
+ * rather than editing the blob in place, rev-guarded like {@link put}; emptying the set
115
+ * deletes the row through the same guard, so a concurrent save's key cannot be swept away.
65
116
  */
66
117
  async remove(workspaceId, key) {
67
- const entries = await this.resolveValues(workspaceId);
68
- const remaining = entries.filter((entry) => entry.key !== key);
69
- // Nothing matched: answer with the current state rather than rewriting the blob, so a
70
- // duplicate delete is a no-op instead of a pointless re-seal.
71
- if (remaining.length === entries.length)
72
- return this.listStored(workspaceId);
73
- if (remaining.length === 0) {
74
- await this.deps.capabilityCredentialRepository.delete(workspaceId);
75
- return [];
76
- }
77
- return this.write(workspaceId, remaining);
118
+ return this.mutate(workspaceId, null, (entries) => {
119
+ const remaining = entries.filter((entry) => entry.key !== key);
120
+ // Nothing matched: answer with the current state rather than rewriting the blob, so a
121
+ // duplicate delete is a no-op instead of a pointless re-seal.
122
+ if (remaining.length === entries.length)
123
+ return null;
124
+ return remaining;
125
+ });
78
126
  }
79
127
  /**
80
128
  * The DECRYPTED credential set for a workspace — read once per dispatch by the composed
@@ -82,24 +130,61 @@ export class CapabilityCredentialsService {
82
130
  * fallback (where a facade wired one) should answer instead.
83
131
  */
84
132
  async resolveValues(workspaceId) {
85
- const record = await this.deps.capabilityCredentialRepository.get(workspaceId);
133
+ return this.decryptEntries(await this.deps.capabilityCredentialRepository.get(workspaceId));
134
+ }
135
+ /**
136
+ * Apply a pure single-key mutation under OPTIMISTIC CONCURRENCY: load, run `apply`, then
137
+ * `compareAndSwap` (or the rev-guarded delete when the set empties); on a lost race reload and
138
+ * re-apply on the winner's snapshot, bounded by {@link MAX_MUTATE_ATTEMPTS}.
139
+ *
140
+ * `apply` returns the next entry set, or null for "nothing to change" (a duplicate delete),
141
+ * which answers with the current summary untouched. `touchedKey` is the one key whose
142
+ * `updatedAt` this write may honestly stamp: "last set" is a per-key fact, so every OTHER
143
+ * entry keeps the timestamp the stored summary already carries. A write that re-stamped the
144
+ * whole set would falsify every neighbour's date.
145
+ */
146
+ async mutate(workspaceId, touchedKey, apply) {
147
+ const repo = this.deps.capabilityCredentialRepository;
148
+ for (let attempt = 0; attempt < MAX_MUTATE_ATTEMPTS; attempt++) {
149
+ const record = await repo.get(workspaceId);
150
+ const next = apply(await this.decryptEntries(record));
151
+ if (next === null)
152
+ return this.parseSummary(record);
153
+ if (next.length === 0) {
154
+ if (!record)
155
+ return [];
156
+ if (await repo.deleteIfRev(workspaceId, record.rev))
157
+ return [];
158
+ continue;
159
+ }
160
+ const now = this.deps.clock.now();
161
+ const prior = new Map(this.parseSummary(record).map((ref) => [ref.key, ref.updatedAt]));
162
+ const summary = next.map((entry) => ({
163
+ key: entry.key,
164
+ updatedAt: entry.key === touchedKey ? now : (prior.get(entry.key) ?? now),
165
+ }));
166
+ const swapped = await repo.compareAndSwap({
167
+ workspaceId,
168
+ credentials: await this.deps.secretCipher.encrypt(JSON.stringify(next)),
169
+ summary: JSON.stringify(summary),
170
+ rev: (record?.rev ?? -1) + 1,
171
+ createdAt: record?.createdAt ?? now,
172
+ updatedAt: now,
173
+ }, record?.rev ?? null);
174
+ if (swapped)
175
+ return summary;
176
+ // Lost the race: loop back, reload, re-apply on the winner's snapshot.
177
+ }
178
+ // No `reason` code, same call as ReviewContendedError: the SPA has no conflict-specific
179
+ // handling beyond "retry", which the message already says.
180
+ throw new ConflictError(`capability credentials for workspace '${workspaceId}' are being modified concurrently; retry`);
181
+ }
182
+ /** Decrypt and validate the sealed entry blob; an absent row is an empty set. */
183
+ async decryptEntries(record) {
86
184
  if (!record)
87
185
  return [];
88
186
  return parseCapabilityCredentialEntries(JSON.parse(await this.deps.secretCipher.decrypt(record.credentials)));
89
187
  }
90
- async write(workspaceId, entries) {
91
- const now = this.deps.clock.now();
92
- const existing = await this.deps.capabilityCredentialRepository.get(workspaceId);
93
- const summary = capabilityCredentialsSummary(entries, now);
94
- await this.deps.capabilityCredentialRepository.upsert({
95
- workspaceId,
96
- credentials: await this.deps.secretCipher.encrypt(JSON.stringify(entries)),
97
- summary: JSON.stringify(summary),
98
- createdAt: existing?.createdAt ?? now,
99
- updatedAt: now,
100
- });
101
- return summary;
102
- }
103
188
  /** Parse the persisted non-secret summary, tolerating a corrupt row (the view still loads). */
104
189
  parseSummary(record) {
105
190
  if (!record)
@@ -1 +1 @@
1
- {"version":3,"file":"CapabilityCredentialsService.js","sourceRoot":"","sources":["../../../src/modules/capabilityCredentials/CapabilityCredentialsService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CapabilityCredentialsService.js","sourceRoot":"","sources":["../../../src/modules/capabilityCredentials/CapabilityCredentialsService.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,26 @@
1
- import type { Clock } from '@cat-factory/kernel';
2
- import type { EnvironmentRegistryRepository } from '@cat-factory/kernel';
1
+ import type { Clock, Logger, ProvisioningOutcome } from '@cat-factory/kernel';
2
+ import type { EnvironmentRecord, EnvironmentRegistryRepository } from '@cat-factory/kernel';
3
3
  import type { SecretCipher } from '@cat-factory/kernel';
4
4
  import type { EnvironmentHandle } from '@cat-factory/kernel';
5
5
  import type { EnvironmentConnectionService } from './EnvironmentConnectionService.js';
6
6
  import type { ProvisioningLogRecorder } from '../provisioning-logs/ProvisioningLogService.js';
7
+ /**
8
+ * Notified after a teardown ATTEMPT has been recorded in the provisioning log, whether it
9
+ * succeeded or failed. Its one consumer today is the run's PR verification report, whose
10
+ * environment-lifecycle proof cannot be closed by the run itself: the TTL sweep that reclaims the
11
+ * environment fires long after the last step settled, so without this edge the PR says "still
12
+ * live" about an environment the platform destroyed on schedule.
13
+ *
14
+ * The FAILURE edge matters as much as the success one, and for the same reason. A run that has
15
+ * settled has no step hook left to fire, so an environment the provider refuses to reclaim would
16
+ * otherwise sit on the PR as "nobody has torn this down yet" rather than as the thing an operator
17
+ * has to go and do: the same unreachable-leg hole one state over. The hook is therefore fired
18
+ * from the ONE place that records the attempt, so a future third teardown path cannot forget it.
19
+ *
20
+ * Strictly best-effort and swallowed by the service: reclaiming infrastructure must never
21
+ * depend on a downstream bookkeeping write.
22
+ */
23
+ export type EnvironmentTeardownRecordedHook = (record: EnvironmentRecord, outcome: ProvisioningOutcome) => Promise<void>;
7
24
  export interface EnvironmentTeardownServiceDependencies {
8
25
  connectionService: EnvironmentConnectionService;
9
26
  environmentRegistryRepository: EnvironmentRegistryRepository;
@@ -11,16 +28,34 @@ export interface EnvironmentTeardownServiceDependencies {
11
28
  clock: Clock;
12
29
  /** Best-effort provisioning-event log; absent ⇒ teardown is unchanged. */
13
30
  provisioningLog?: ProvisioningLogRecorder;
31
+ /** Structured logger for the best-effort hook below; absent ⇒ its failures are unreported. */
32
+ logger?: Logger;
14
33
  }
15
34
  export declare class EnvironmentTeardownService {
16
35
  private readonly deps;
36
+ private readonly log;
37
+ /**
38
+ * Late-bound because the engine that consumes it is constructed AFTER this service (it is
39
+ * injected into the provisioning service, which the engine takes). Same shape as
40
+ * `setInitiativeLoop` in the composition root, and for the same reason.
41
+ */
42
+ private teardownRecordedHook;
17
43
  constructor(deps: EnvironmentTeardownServiceDependencies);
44
+ /** Late-bind the teardown-recorded notification. Unset ⇒ nothing is notified. */
45
+ setTeardownRecordedHook(hook: EnvironmentTeardownRecordedHook | undefined): void;
18
46
  /** Tear down a single environment and tombstone its record. */
19
47
  teardown(workspaceId: string, id: string): Promise<EnvironmentHandle>;
20
48
  /** Tear down every environment whose TTL has elapsed. Returns the count swept. */
21
49
  sweepExpired(now: number): Promise<number>;
22
50
  private teardownRecord;
23
- private logTeardown;
51
+ /**
52
+ * Append the attempt to the provisioning log and then notify, in that order and in ONE place.
53
+ * The ordering is what makes the notification usable: its consumer reads the log back, so a
54
+ * hook fired before the row landed would report the state the edge exists to correct. Keeping
55
+ * both here is what makes the FAILURE edge structural rather than a line someone remembered to
56
+ * add beside the success one.
57
+ */
58
+ private recordTeardownAttempt;
24
59
  private decryptFields;
25
60
  }
26
61
  //# sourceMappingURL=EnvironmentTeardownService.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EnvironmentTeardownService.d.ts","sourceRoot":"","sources":["../../../src/modules/environments/EnvironmentTeardownService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,EAAqB,6BAA6B,EAAE,MAAM,qBAAqB,CAAA;AAC3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAA;AAErF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gDAAgD,CAAA;AAO7F,MAAM,WAAW,sCAAsC;IACrD,iBAAiB,EAAE,4BAA4B,CAAA;IAC/C,6BAA6B,EAAE,6BAA6B,CAAA;IAC5D,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;IACZ,0EAA0E;IAC1E,eAAe,CAAC,EAAE,uBAAuB,CAAA;CAC1C;AAED,qBAAa,0BAA0B;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,sCAAsC,EAAI;IAE7E,+DAA+D;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAQ1E;IAED,kFAAkF;IAC5E,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAa/C;YAEa,cAAc;YAqCd,WAAW;YAmBX,aAAa;CAK5B"}
1
+ {"version":3,"file":"EnvironmentTeardownService.d.ts","sourceRoot":"","sources":["../../../src/modules/environments/EnvironmentTeardownService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,6BAA6B,EAAE,MAAM,qBAAqB,CAAA;AAC3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAA;AAErF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gDAAgD,CAAA;AAO7F;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,+BAA+B,GAAG,CAC5C,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,mBAAmB,KACzB,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB,MAAM,WAAW,sCAAsC;IACrD,iBAAiB,EAAE,4BAA4B,CAAA;IAC/C,6BAA6B,EAAE,6BAA6B,CAAA;IAC5D,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;IACZ,0EAA0E;IAC1E,eAAe,CAAC,EAAE,uBAAuB,CAAA;IACzC,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,qBAAa,0BAA0B;IASzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IARjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAC5B;;;;OAIG;IACH,OAAO,CAAC,oBAAoB,CAA6C;IAEzE,YAA6B,IAAI,EAAE,sCAAsC,EAExE;IAED,iFAAiF;IACjF,uBAAuB,CAAC,IAAI,EAAE,+BAA+B,GAAG,SAAS,GAAG,IAAI,CAE/E;IAED,+DAA+D;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAQ1E;IAED,kFAAkF;IAC5E,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAa/C;YAEa,cAAc;IAqC5B;;;;;;OAMG;YACW,qBAAqB;YA2BrB,aAAa;CAK5B"}
@@ -1,9 +1,21 @@
1
- import { assertFound } from '@cat-factory/kernel';
1
+ import { assertFound, noopLogger, runBestEffort } from '@cat-factory/kernel';
2
2
  import { recordToHandle } from './environments.logic.js';
3
3
  export class EnvironmentTeardownService {
4
4
  deps;
5
+ log;
6
+ /**
7
+ * Late-bound because the engine that consumes it is constructed AFTER this service (it is
8
+ * injected into the provisioning service, which the engine takes). Same shape as
9
+ * `setInitiativeLoop` in the composition root, and for the same reason.
10
+ */
11
+ teardownRecordedHook;
5
12
  constructor(deps) {
6
13
  this.deps = deps;
14
+ this.log = deps.logger ?? noopLogger;
15
+ }
16
+ /** Late-bind the teardown-recorded notification. Unset ⇒ nothing is notified. */
17
+ setTeardownRecordedHook(hook) {
18
+ this.teardownRecordedHook = hook;
7
19
  }
8
20
  /** Tear down a single environment and tombstone its record. */
9
21
  async teardown(workspaceId, id) {
@@ -49,14 +61,21 @@ export class EnvironmentTeardownService {
49
61
  // Log the verbatim provider error before it propagates (the sweep swallows
50
62
  // it; an on-demand teardown surfaces it). The local record is NOT tombstoned
51
63
  // on a provider failure, matching the existing retry-next-pass behaviour.
52
- await this.logTeardown(record, 'failure', error instanceof Error ? error.message : String(error));
64
+ await this.recordTeardownAttempt(record, 'failure', error instanceof Error ? error.message : String(error));
53
65
  throw error;
54
66
  }
55
67
  }
56
68
  await this.deps.environmentRegistryRepository.softDelete(record.workspaceId, record.id, this.deps.clock.now());
57
- await this.logTeardown(record, 'success', null);
69
+ await this.recordTeardownAttempt(record, 'success', null);
58
70
  }
59
- async logTeardown(record, outcome, error) {
71
+ /**
72
+ * Append the attempt to the provisioning log and then notify, in that order and in ONE place.
73
+ * The ordering is what makes the notification usable: its consumer reads the log back, so a
74
+ * hook fired before the row landed would report the state the edge exists to correct. Keeping
75
+ * both here is what makes the FAILURE edge structural rather than a line someone remembered to
76
+ * add beside the success one.
77
+ */
78
+ async recordTeardownAttempt(record, outcome, error) {
60
79
  await this.deps.provisioningLog?.record({
61
80
  workspaceId: record.workspaceId,
62
81
  subsystem: 'environment',
@@ -69,6 +88,15 @@ export class EnvironmentTeardownService {
69
88
  error,
70
89
  detail: null,
71
90
  });
91
+ const hook = this.teardownRecordedHook;
92
+ if (!hook)
93
+ return;
94
+ await runBestEffort(this.log, 'environment.teardownRecordedHook', () => hook(record, outcome), {
95
+ workspaceId: record.workspaceId,
96
+ environmentId: record.id,
97
+ executionId: record.executionId,
98
+ outcome,
99
+ });
72
100
  }
73
101
  async decryptFields(cipher) {
74
102
  if (!cipher)
@@ -1 +1 @@
1
- {"version":3,"file":"EnvironmentTeardownService.js","sourceRoot":"","sources":["../../../src/modules/environments/EnvironmentTeardownService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"EnvironmentTeardownService.js","sourceRoot":"","sources":["../../../src/modules/environments/EnvironmentTeardownService.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/integrations",
3
- "version": "0.118.1",
3
+ "version": "0.120.0",
4
4
  "description": "External-system integration domain logic for the Agent Architecture Board (GitHub, documents, tasks, environments, runners).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,15 +27,15 @@
27
27
  "ai": "^7.0.47",
28
28
  "p-map": "^7.0.6",
29
29
  "yaml": "^2.9.0",
30
- "@cat-factory/contracts": "0.216.0",
31
- "@cat-factory/kernel": "0.219.0"
30
+ "@cat-factory/contracts": "0.218.0",
31
+ "@cat-factory/kernel": "0.221.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "typescript": "7.0.2",
35
35
  "undici": "^8.9.0",
36
36
  "valibot": "^1.4.2",
37
37
  "vitest": "^4.1.10",
38
- "@cat-factory/caching": "0.13.4"
38
+ "@cat-factory/caching": "0.13.6"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -b tsconfig.build.json",