@skillsmith/core 0.11.4 → 0.11.6
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/CHANGELOG.md +60 -0
- package/README.md +10 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +20 -5
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/api/client.private-registry.d.ts +80 -0
- package/dist/src/api/client.private-registry.d.ts.map +1 -0
- package/dist/src/api/client.private-registry.js +97 -0
- package/dist/src/api/client.private-registry.js.map +1 -0
- package/dist/src/api/client.private-registry.test.d.ts +9 -0
- package/dist/src/api/client.private-registry.test.d.ts.map +1 -0
- package/dist/src/api/client.private-registry.test.js +127 -0
- package/dist/src/api/client.private-registry.test.js.map +1 -0
- package/dist/src/api/client.test.js +39 -0
- package/dist/src/api/client.test.js.map +1 -1
- package/dist/src/api/client.toSkill.test.d.ts +25 -0
- package/dist/src/api/client.toSkill.test.d.ts.map +1 -0
- package/dist/src/api/client.toSkill.test.js +97 -0
- package/dist/src/api/client.toSkill.test.js.map +1 -0
- package/dist/src/api/client.token-refresh.d.ts +8 -0
- package/dist/src/api/client.token-refresh.d.ts.map +1 -1
- package/dist/src/api/client.token-refresh.js +26 -0
- package/dist/src/api/client.token-refresh.js.map +1 -1
- package/dist/src/api/client.token-refresh.test.js +82 -0
- package/dist/src/api/client.token-refresh.test.js.map +1 -1
- package/dist/src/api/index.d.ts +2 -0
- package/dist/src/api/index.d.ts.map +1 -1
- package/dist/src/api/index.js +9 -0
- package/dist/src/api/index.js.map +1 -1
- package/dist/src/api/security-summary.d.ts +73 -0
- package/dist/src/api/security-summary.d.ts.map +1 -0
- package/dist/src/api/security-summary.js +88 -0
- package/dist/src/api/security-summary.js.map +1 -0
- package/dist/src/api/security-summary.test.d.ts +9 -0
- package/dist/src/api/security-summary.test.d.ts.map +1 -0
- package/dist/src/api/security-summary.test.js +94 -0
- package/dist/src/api/security-summary.test.js.map +1 -0
- package/dist/src/compatibility/slugs.d.ts +7 -3
- package/dist/src/compatibility/slugs.d.ts.map +1 -1
- package/dist/src/compatibility/slugs.js +11 -4
- package/dist/src/compatibility/slugs.js.map +1 -1
- package/dist/src/config/config-atomic-write.d.ts +30 -10
- package/dist/src/config/config-atomic-write.d.ts.map +1 -1
- package/dist/src/config/config-atomic-write.js +33 -80
- package/dist/src/config/config-atomic-write.js.map +1 -1
- package/dist/src/config/config-atomic-write.test.d.ts +8 -0
- package/dist/src/config/config-atomic-write.test.d.ts.map +1 -1
- package/dist/src/config/config-atomic-write.test.js +64 -12
- package/dist/src/config/config-atomic-write.test.js.map +1 -1
- package/dist/src/config/owned-lock.acquire.d.ts +53 -0
- package/dist/src/config/owned-lock.acquire.d.ts.map +1 -0
- package/dist/src/config/owned-lock.acquire.js +140 -0
- package/dist/src/config/owned-lock.acquire.js.map +1 -0
- package/dist/src/config/owned-lock.claim.d.ts +84 -0
- package/dist/src/config/owned-lock.claim.d.ts.map +1 -0
- package/dist/src/config/owned-lock.claim.js +310 -0
- package/dist/src/config/owned-lock.claim.js.map +1 -0
- package/dist/src/config/owned-lock.d.ts +113 -0
- package/dist/src/config/owned-lock.d.ts.map +1 -0
- package/dist/src/config/owned-lock.js +121 -0
- package/dist/src/config/owned-lock.js.map +1 -0
- package/dist/src/config/owned-lock.test.d.ts +12 -0
- package/dist/src/config/owned-lock.test.d.ts.map +1 -0
- package/dist/src/config/owned-lock.test.js +412 -0
- package/dist/src/config/owned-lock.test.js.map +1 -0
- package/dist/src/config/owned-lock.types.d.ts +78 -0
- package/dist/src/config/owned-lock.types.d.ts.map +1 -0
- package/dist/src/config/owned-lock.types.js +30 -0
- package/dist/src/config/owned-lock.types.js.map +1 -0
- package/dist/src/db/createDatabase.d.ts.map +1 -1
- package/dist/src/db/createDatabase.js +19 -7
- package/dist/src/db/createDatabase.js.map +1 -1
- package/dist/src/db/drivers/sqljsDriver.d.ts +8 -0
- package/dist/src/db/drivers/sqljsDriver.d.ts.map +1 -1
- package/dist/src/db/drivers/sqljsDriver.js +24 -2
- package/dist/src/db/drivers/sqljsDriver.js.map +1 -1
- package/dist/src/db/migration-runner.d.ts.map +1 -1
- package/dist/src/db/migration-runner.js +23 -2
- package/dist/src/db/migration-runner.js.map +1 -1
- package/dist/src/db/schema.d.ts.map +1 -1
- package/dist/src/db/schema.js +29 -3
- package/dist/src/db/schema.js.map +1 -1
- package/dist/src/embeddings/hnsw-store.exports.d.ts +11 -0
- package/dist/src/embeddings/hnsw-store.exports.d.ts.map +1 -0
- package/dist/src/embeddings/hnsw-store.exports.js +10 -0
- package/dist/src/embeddings/hnsw-store.exports.js.map +1 -0
- package/dist/src/embeddings/index.d.ts +1 -2
- package/dist/src/embeddings/index.d.ts.map +1 -1
- package/dist/src/embeddings/index.js +17 -3
- package/dist/src/embeddings/index.js.map +1 -1
- package/dist/src/embeddings/probe.d.ts.map +1 -1
- package/dist/src/embeddings/probe.js +4 -8
- package/dist/src/embeddings/probe.js.map +1 -1
- package/dist/src/exports/services.d.ts +5 -1
- package/dist/src/exports/services.d.ts.map +1 -1
- package/dist/src/exports/services.js +27 -0
- package/dist/src/exports/services.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/index.d.ts +2 -2
- package/dist/src/install/index.d.ts.map +1 -1
- package/dist/src/install/index.js +1 -1
- package/dist/src/install/index.js.map +1 -1
- package/dist/src/install/paths.d.ts +165 -1
- package/dist/src/install/paths.d.ts.map +1 -1
- package/dist/src/install/paths.js +254 -1
- package/dist/src/install/paths.js.map +1 -1
- package/dist/src/install/paths.test.js +174 -2
- package/dist/src/install/paths.test.js.map +1 -1
- package/dist/src/security/scanner/multiline-category-closure.test.d.ts +80 -0
- package/dist/src/security/scanner/multiline-category-closure.test.d.ts.map +1 -0
- package/dist/src/security/scanner/multiline-category-closure.test.js +287 -0
- package/dist/src/security/scanner/multiline-category-closure.test.js.map +1 -0
- package/dist/src/services/context-words.d.ts +40 -0
- package/dist/src/services/context-words.d.ts.map +1 -0
- package/dist/src/services/context-words.js +178 -0
- package/dist/src/services/context-words.js.map +1 -0
- package/dist/src/services/context-words.test.d.ts +2 -0
- package/dist/src/services/context-words.test.d.ts.map +1 -0
- package/dist/src/services/context-words.test.js +94 -0
- package/dist/src/services/context-words.test.js.map +1 -0
- package/dist/src/services/recommend-guard.d.ts +28 -0
- package/dist/src/services/recommend-guard.d.ts.map +1 -0
- package/dist/src/services/recommend-guard.js +34 -0
- package/dist/src/services/recommend-guard.js.map +1 -0
- package/dist/src/services/recommend-guard.test.d.ts +2 -0
- package/dist/src/services/recommend-guard.test.d.ts.map +1 -0
- package/dist/src/services/recommend-guard.test.js +28 -0
- package/dist/src/services/recommend-guard.test.js.map +1 -0
- package/dist/src/services/skill-installation.content.d.ts +96 -0
- package/dist/src/services/skill-installation.content.d.ts.map +1 -0
- package/dist/src/services/skill-installation.content.js +319 -0
- package/dist/src/services/skill-installation.content.js.map +1 -0
- package/dist/src/services/skill-installation.helpers.d.ts +32 -1
- package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
- package/dist/src/services/skill-installation.helpers.js +62 -7
- package/dist/src/services/skill-installation.helpers.js.map +1 -1
- package/dist/src/services/skill-installation.helpers.test.js +108 -2
- package/dist/src/services/skill-installation.helpers.test.js.map +1 -1
- package/dist/src/services/skill-installation.io.d.ts +22 -1
- package/dist/src/services/skill-installation.io.d.ts.map +1 -1
- package/dist/src/services/skill-installation.io.js +110 -6
- package/dist/src/services/skill-installation.io.js.map +1 -1
- package/dist/src/services/skill-installation.service.d.ts +22 -1
- package/dist/src/services/skill-installation.service.d.ts.map +1 -1
- package/dist/src/services/skill-installation.service.js +30 -5
- package/dist/src/services/skill-installation.service.js.map +1 -1
- package/dist/src/services/skill-installation.types.d.ts +42 -1
- package/dist/src/services/skill-installation.types.d.ts.map +1 -1
- package/dist/src/services/skill-installation.types.js.map +1 -1
- package/dist/src/services/skill-manifest.d.ts +21 -0
- package/dist/src/services/skill-manifest.d.ts.map +1 -1
- package/dist/src/services/skill-manifest.js +51 -6
- package/dist/src/services/skill-manifest.js.map +1 -1
- package/dist/src/services/skill-manifest.test.d.ts +2 -0
- package/dist/src/services/skill-manifest.test.d.ts.map +1 -0
- package/dist/src/services/skill-manifest.test.js +202 -0
- package/dist/src/services/skill-manifest.test.js.map +1 -0
- package/dist/src/services/skill-resolution.d.ts +67 -0
- package/dist/src/services/skill-resolution.d.ts.map +1 -0
- package/dist/src/services/skill-resolution.js +67 -0
- package/dist/src/services/skill-resolution.js.map +1 -0
- package/dist/src/services/skill-resolution.test.d.ts +2 -0
- package/dist/src/services/skill-resolution.test.d.ts.map +1 -0
- package/dist/src/services/skill-resolution.test.js +143 -0
- package/dist/src/services/skill-resolution.test.js.map +1 -0
- package/dist/src/types/skill.d.ts +10 -0
- package/dist/src/types/skill.d.ts.map +1 -1
- package/dist/src/types.d.ts +9 -5
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/index.d.ts +1 -0
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +4 -0
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/quiet-mode.d.ts +21 -0
- package/dist/src/utils/quiet-mode.d.ts.map +1 -0
- package/dist/src/utils/quiet-mode.js +26 -0
- package/dist/src/utils/quiet-mode.js.map +1 -0
- package/dist/tests/EmbeddingService.test.js +58 -1
- package/dist/tests/EmbeddingService.test.js.map +1 -1
- package/dist/tests/SecurityScanner.performance.test.js +5 -2
- package/dist/tests/SecurityScanner.performance.test.js.map +1 -1
- package/dist/tests/db/database-abstraction.test.js +45 -1
- package/dist/tests/db/database-abstraction.test.js.map +1 -1
- package/dist/tests/db/migration-runner-race.test.d.ts +2 -0
- package/dist/tests/db/migration-runner-race.test.d.ts.map +1 -0
- package/dist/tests/db/migration-runner-race.test.js +75 -0
- package/dist/tests/db/migration-runner-race.test.js.map +1 -0
- package/dist/tests/db/schema-async.test.js +37 -1
- package/dist/tests/db/schema-async.test.js.map +1 -1
- package/dist/tests/db/sqljsDriver.test.js +63 -1
- package/dist/tests/db/sqljsDriver.test.js.map +1 -1
- package/dist/tests/helpers/deterministic-dead-pid.d.ts +13 -0
- package/dist/tests/helpers/deterministic-dead-pid.d.ts.map +1 -0
- package/dist/tests/helpers/deterministic-dead-pid.js +31 -0
- package/dist/tests/helpers/deterministic-dead-pid.js.map +1 -0
- package/dist/tests/integration/owned-lock-lost-update.test.d.ts +12 -0
- package/dist/tests/integration/owned-lock-lost-update.test.d.ts.map +1 -0
- package/dist/tests/integration/owned-lock-lost-update.test.js +52 -0
- package/dist/tests/integration/owned-lock-lost-update.test.js.map +1 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts +19 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts.map +1 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.js +177 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.js.map +1 -0
- package/dist/tests/schema.test.js +43 -1
- package/dist/tests/schema.test.js.map +1 -1
- package/dist/tests/unit/services/skill-installation.content.test.d.ts +10 -0
- package/dist/tests/unit/services/skill-installation.content.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.content.test.js +290 -0
- package/dist/tests/unit/services/skill-installation.content.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.d.ts +34 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js +246 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts +8 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.js +72 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.d.ts +17 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.js +112 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts +15 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.js +190 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts +17 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.js +126 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.test.js +7 -112
- package/dist/tests/unit/services/skill-installation.service.test.js.map +1 -1
- package/dist/tests/utils/quiet-mode.test.d.ts +12 -0
- package/dist/tests/utils/quiet-mode.test.d.ts.map +1 -0
- package/dist/tests/utils/quiet-mode.test.js +42 -0
- package/dist/tests/utils/quiet-mode.test.js.map +1 -0
- package/package.json +17 -5
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim parsing and lock-file mechanics (exclusive creation, reclaim,
|
|
3
|
+
* release) for the owned-lock primitive.
|
|
4
|
+
* @module @skillsmith/core/config/owned-lock.claim
|
|
5
|
+
* @see owned-lock.ts for the full soundness argument and the PUBLIC API.
|
|
6
|
+
* @see owned-lock.acquire.ts for the core acquire loop + `StuckLockError`,
|
|
7
|
+
* split into a sibling file purely to keep both under the repo's
|
|
8
|
+
* 500-line-per-file gate.
|
|
9
|
+
*
|
|
10
|
+
* INTERNAL module -- not part of the public surface (no `package.json`
|
|
11
|
+
* subpath export). Co-located tests and the cross-process race-test child
|
|
12
|
+
* harness import directly from here, by relative path, specifically to
|
|
13
|
+
* reach `createLockExclusive`'s `linkSyncOverride` test seam -- see
|
|
14
|
+
* `owned-lock.acquire.ts` for the analogous rationale on the acquire loop's
|
|
15
|
+
* own destructive test-only options.
|
|
16
|
+
*/
|
|
17
|
+
import type { Claim, ReclaimOutcome, RefusalCategory } from './owned-lock.types.js';
|
|
18
|
+
export declare function randomHex(bytes: number): string;
|
|
19
|
+
/**
|
|
20
|
+
* Synchronously block the calling thread for `ms` milliseconds without
|
|
21
|
+
* spinning the CPU. `Atomics.wait` on a throwaway `SharedArrayBuffer` is the
|
|
22
|
+
* standard synchronous-sleep primitive in Node (unlike browsers, Node does
|
|
23
|
+
* not forbid calling it on the main thread).
|
|
24
|
+
*/
|
|
25
|
+
export declare function sleepSync(ms: number): void;
|
|
26
|
+
/**
|
|
27
|
+
* Parse raw lock-file text into a {@link Claim}. A v1 claim is one line of
|
|
28
|
+
* canonical JSON; a legacy claim is a bare decimal integer (today's
|
|
29
|
+
* `acquireConfigLock` format, `String(process.pid)`); anything else --
|
|
30
|
+
* including valid JSON at a DIFFERENT `v` -- is `unparseable` and therefore
|
|
31
|
+
* NEVER auto-reclaimed.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseClaim(text: string): Claim;
|
|
34
|
+
/**
|
|
35
|
+
* Bounded, TOCTOU-free claim read: open once, `fstat` THAT fd (not the
|
|
36
|
+
* path), refuse above {@link MAX_LOCK_BYTES}, read from the same fd. Returns
|
|
37
|
+
* `{ kind: 'absent' }` on `ENOENT` (or any other open failure).
|
|
38
|
+
*/
|
|
39
|
+
export declare function readClaim(path: string): Claim;
|
|
40
|
+
export declare function isAutoReclaimDisabled(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Conservative in every ambiguous direction: PID reuse and a not-our-signal
|
|
43
|
+
* `EPERM` both read as ALIVE (declining to reclaim costs a timeout; an
|
|
44
|
+
* incorrect reclaim costs a lost caller). `pid <= 0` is rejected WITHOUT
|
|
45
|
+
* probing -- `kill(0, 0)` signals the process GROUP, which would make the
|
|
46
|
+
* liveness probe meaningless.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isOwnerDefinitelyDead(claim: Claim, killProbe?: typeof process.kill): boolean;
|
|
49
|
+
export declare function classifyRefusal(claim: Claim): RefusalCategory;
|
|
50
|
+
/**
|
|
51
|
+
* Create `path` exclusively via a temp file + `linkSync` (atomic; `EEXIST`
|
|
52
|
+
* if `path` exists) so a lock file is NEVER observable without a complete
|
|
53
|
+
* claim -- closing R2 (a writer could otherwise crash between create and
|
|
54
|
+
* write, leaving a permanently unreclaimable main lock). On filesystems
|
|
55
|
+
* without hardlink support this throws rather than falling back to a
|
|
56
|
+
* non-atomic `openSync('wx')` + separate write -- that two-step sequence
|
|
57
|
+
* would itself reopen R2 (an observer between the two steps, or a crash in
|
|
58
|
+
* between, sees an empty/truncated lock that is then PERMANENTLY
|
|
59
|
+
* unreclaimable, since an `unparseable` claim is never auto-reclaimed by
|
|
60
|
+
* design). Failing closed on an unsupported filesystem is the sound
|
|
61
|
+
* resolution the reviewer required; hardlink support is effectively
|
|
62
|
+
* universal on the filesystems Node actually runs on.
|
|
63
|
+
*
|
|
64
|
+
* @param linkSyncOverride - @internal test seam (owned-lock.test.ts item 14)
|
|
65
|
+
* to exercise the hardlink-unavailable fail-closed path deterministically.
|
|
66
|
+
* Never set outside that test.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createLockExclusive(path: string, recordJson: string, linkSyncOverride?: (existingPath: string, newPath: string) => void): boolean;
|
|
69
|
+
export interface ReclaimInternalOptions {
|
|
70
|
+
/** @internal NEGATIVE CONTROL ONLY (owned-lock-reclaim-race.test.ts) -- removes the authoritative re-read that makes this mechanism sound. Never set outside that spec. */
|
|
71
|
+
unsafeSkipRevalidation?: boolean;
|
|
72
|
+
linkSyncOverride?: (existingPath: string, newPath: string) => void;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validate and destroy a stale main lock inside a region from which every
|
|
76
|
+
* other reclaimer is excluded (the reclaim lock). This is the ONLY code path
|
|
77
|
+
* that may ever unlink `<target>.lock` on the strength of a liveness
|
|
78
|
+
* inference: a caller's own pre-filter is advisory and discarded; only the
|
|
79
|
+
* re-read performed HERE, under the reclaim lock, authorizes the unlink.
|
|
80
|
+
*/
|
|
81
|
+
export declare function tryReclaimUnderLock(lockPath: string, reclaimPath: string, opts: ReclaimInternalOptions): ReclaimOutcome;
|
|
82
|
+
export declare function releaseOwned(path: string, token: string): void;
|
|
83
|
+
export declare function makeRelease(lockPath: string, token: string): () => void;
|
|
84
|
+
//# sourceMappingURL=owned-lock.claim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owned-lock.claim.d.ts","sourceRoot":"","sources":["../../../src/config/owned-lock.claim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAsBH,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEnF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAG1C;AAMD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAiC9C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAkB7C;AAED,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,EACZ,SAAS,GAAE,OAAO,OAAO,CAAC,IAAmB,GAC5C,OAAO,CAYT;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAK7D;AAyCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,gBAAgB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GACjE,OAAO,CA0BT;AAMD,MAAM,WAAW,sBAAsB;IACrC,2KAA2K;IAC3K,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,gBAAgB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACnE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,sBAAsB,GAC3B,cAAc,CAmChB;AAMD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAmB9D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,IAAI,CAOvE"}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim parsing and lock-file mechanics (exclusive creation, reclaim,
|
|
3
|
+
* release) for the owned-lock primitive.
|
|
4
|
+
* @module @skillsmith/core/config/owned-lock.claim
|
|
5
|
+
* @see owned-lock.ts for the full soundness argument and the PUBLIC API.
|
|
6
|
+
* @see owned-lock.acquire.ts for the core acquire loop + `StuckLockError`,
|
|
7
|
+
* split into a sibling file purely to keep both under the repo's
|
|
8
|
+
* 500-line-per-file gate.
|
|
9
|
+
*
|
|
10
|
+
* INTERNAL module -- not part of the public surface (no `package.json`
|
|
11
|
+
* subpath export). Co-located tests and the cross-process race-test child
|
|
12
|
+
* harness import directly from here, by relative path, specifically to
|
|
13
|
+
* reach `createLockExclusive`'s `linkSyncOverride` test seam -- see
|
|
14
|
+
* `owned-lock.acquire.ts` for the analogous rationale on the acquire loop's
|
|
15
|
+
* own destructive test-only options.
|
|
16
|
+
*/
|
|
17
|
+
import { randomBytes } from 'node:crypto';
|
|
18
|
+
import { chmodSync, closeSync, fstatSync, linkSync, openSync, readSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
19
|
+
import { hostname } from 'node:os';
|
|
20
|
+
import { basename, dirname, join } from 'node:path';
|
|
21
|
+
import { HARDLINK_UNAVAILABLE_CODES, LOCK_RETRY_DELAY_MS, MAX_LOCK_BYTES, RECLAIM_LOCK_TIMEOUT_MS, } from './owned-lock.types.js';
|
|
22
|
+
export function randomHex(bytes) {
|
|
23
|
+
return randomBytes(bytes).toString('hex');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Synchronously block the calling thread for `ms` milliseconds without
|
|
27
|
+
* spinning the CPU. `Atomics.wait` on a throwaway `SharedArrayBuffer` is the
|
|
28
|
+
* standard synchronous-sleep primitive in Node (unlike browsers, Node does
|
|
29
|
+
* not forbid calling it on the main thread).
|
|
30
|
+
*/
|
|
31
|
+
export function sleepSync(ms) {
|
|
32
|
+
const view = new Int32Array(new SharedArrayBuffer(4));
|
|
33
|
+
Atomics.wait(view, 0, 0, ms);
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Claim parsing + bounded, TOCTOU-free read
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/**
|
|
39
|
+
* Parse raw lock-file text into a {@link Claim}. A v1 claim is one line of
|
|
40
|
+
* canonical JSON; a legacy claim is a bare decimal integer (today's
|
|
41
|
+
* `acquireConfigLock` format, `String(process.pid)`); anything else --
|
|
42
|
+
* including valid JSON at a DIFFERENT `v` -- is `unparseable` and therefore
|
|
43
|
+
* NEVER auto-reclaimed.
|
|
44
|
+
*/
|
|
45
|
+
export function parseClaim(text) {
|
|
46
|
+
const trimmed = text.trim();
|
|
47
|
+
if (trimmed.length === 0)
|
|
48
|
+
return { kind: 'unparseable' };
|
|
49
|
+
if (/^[0-9]+$/.test(trimmed)) {
|
|
50
|
+
const pid = Number(trimmed);
|
|
51
|
+
if (Number.isSafeInteger(pid))
|
|
52
|
+
return { kind: 'legacy', pid };
|
|
53
|
+
}
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(text);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { kind: 'unparseable' };
|
|
60
|
+
}
|
|
61
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
62
|
+
return { kind: 'unparseable' };
|
|
63
|
+
const p = parsed;
|
|
64
|
+
if (p['v'] === 1 &&
|
|
65
|
+
typeof p['pid'] === 'number' &&
|
|
66
|
+
typeof p['token'] === 'string' &&
|
|
67
|
+
typeof p['host'] === 'string' &&
|
|
68
|
+
typeof p['acquiredAt'] === 'number') {
|
|
69
|
+
return {
|
|
70
|
+
kind: 'v1',
|
|
71
|
+
pid: p['pid'],
|
|
72
|
+
token: p['token'],
|
|
73
|
+
host: p['host'],
|
|
74
|
+
acquiredAt: p['acquiredAt'],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { kind: 'unparseable' };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Bounded, TOCTOU-free claim read: open once, `fstat` THAT fd (not the
|
|
81
|
+
* path), refuse above {@link MAX_LOCK_BYTES}, read from the same fd. Returns
|
|
82
|
+
* `{ kind: 'absent' }` on `ENOENT` (or any other open failure).
|
|
83
|
+
*/
|
|
84
|
+
export function readClaim(path) {
|
|
85
|
+
let fd;
|
|
86
|
+
try {
|
|
87
|
+
fd = openSync(path, 'r');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return { kind: 'absent' };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const st = fstatSync(fd);
|
|
94
|
+
if (st.size > MAX_LOCK_BYTES)
|
|
95
|
+
return { kind: 'unparseable' };
|
|
96
|
+
const buf = Buffer.allocUnsafe(MAX_LOCK_BYTES);
|
|
97
|
+
const n = readSync(fd, buf, 0, MAX_LOCK_BYTES, 0);
|
|
98
|
+
return parseClaim(buf.subarray(0, n).toString('utf8'));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { kind: 'unparseable' };
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
closeSync(fd);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function isAutoReclaimDisabled() {
|
|
108
|
+
return process.env['SKILLSMITH_LOCK_NO_AUTO_RECLAIM'] === '1';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Conservative in every ambiguous direction: PID reuse and a not-our-signal
|
|
112
|
+
* `EPERM` both read as ALIVE (declining to reclaim costs a timeout; an
|
|
113
|
+
* incorrect reclaim costs a lost caller). `pid <= 0` is rejected WITHOUT
|
|
114
|
+
* probing -- `kill(0, 0)` signals the process GROUP, which would make the
|
|
115
|
+
* liveness probe meaningless.
|
|
116
|
+
*/
|
|
117
|
+
export function isOwnerDefinitelyDead(claim, killProbe = process.kill) {
|
|
118
|
+
if (isAutoReclaimDisabled())
|
|
119
|
+
return false;
|
|
120
|
+
if (claim.kind !== 'v1')
|
|
121
|
+
return false; // legacy | unparseable | absent -- never (D-5/D-6)
|
|
122
|
+
if (claim.host !== hostname())
|
|
123
|
+
return false;
|
|
124
|
+
if (!Number.isInteger(claim.pid) || claim.pid <= 0)
|
|
125
|
+
return false;
|
|
126
|
+
try {
|
|
127
|
+
killProbe(claim.pid, 0);
|
|
128
|
+
return false; // signalable -> alive
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
if (err.code === 'ESRCH')
|
|
132
|
+
return true; // no such process -> dead
|
|
133
|
+
return false; // EPERM (exists, not ours) or any other errno -> assume alive
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function classifyRefusal(claim) {
|
|
137
|
+
if (isAutoReclaimDisabled())
|
|
138
|
+
return 'reclaim-disabled';
|
|
139
|
+
if (claim.kind === 'legacy')
|
|
140
|
+
return 'legacy';
|
|
141
|
+
if (claim.kind === 'unparseable' || claim.kind === 'absent')
|
|
142
|
+
return 'unparseable';
|
|
143
|
+
return 'held';
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Exclusive creation -- atomic AND content-complete (D-6)
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
/**
|
|
149
|
+
* Create the temp file that {@link createLockExclusive} will `linkSync` into
|
|
150
|
+
* place, EXCLUSIVELY (`wx`) rather than with the default truncating `w`.
|
|
151
|
+
* `randomHex(8)` collisions are astronomically unlikely, but a `w`-flag
|
|
152
|
+
* write is a silent last-writer-wins on collision -- including clobbering
|
|
153
|
+
* another caller's temp file AFTER it has already been `linkSync`'d into a
|
|
154
|
+
* live lock (the temp file's own content no longer matters to that lock at
|
|
155
|
+
* that point, but a `w`-flag collision earlier in the window, before the
|
|
156
|
+
* link, could still corrupt an in-flight sibling attempt). `wx` turns any
|
|
157
|
+
* collision into a loud `EEXIST` retry with a fresh random suffix instead of
|
|
158
|
+
* a silent overwrite.
|
|
159
|
+
*/
|
|
160
|
+
function writeTempClaimExclusive(dir, base, recordJson) {
|
|
161
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
162
|
+
const tmp = join(dir, `.${base}.${randomHex(8)}.tmp`);
|
|
163
|
+
let fd;
|
|
164
|
+
try {
|
|
165
|
+
fd = openSync(tmp, 'wx', 0o600);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (err.code === 'EEXIST')
|
|
169
|
+
continue; // collision -- retry with a fresh suffix
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
writeFileSync(fd, recordJson);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
closeSync(fd);
|
|
177
|
+
}
|
|
178
|
+
chmodSync(tmp, 0o600); // defeat a permissive umask BEFORE the file is reachable as a lock
|
|
179
|
+
return tmp;
|
|
180
|
+
}
|
|
181
|
+
throw new Error(`[skillsmith] could not create a unique temp claim file under ${dir} after 5 attempts`);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Create `path` exclusively via a temp file + `linkSync` (atomic; `EEXIST`
|
|
185
|
+
* if `path` exists) so a lock file is NEVER observable without a complete
|
|
186
|
+
* claim -- closing R2 (a writer could otherwise crash between create and
|
|
187
|
+
* write, leaving a permanently unreclaimable main lock). On filesystems
|
|
188
|
+
* without hardlink support this throws rather than falling back to a
|
|
189
|
+
* non-atomic `openSync('wx')` + separate write -- that two-step sequence
|
|
190
|
+
* would itself reopen R2 (an observer between the two steps, or a crash in
|
|
191
|
+
* between, sees an empty/truncated lock that is then PERMANENTLY
|
|
192
|
+
* unreclaimable, since an `unparseable` claim is never auto-reclaimed by
|
|
193
|
+
* design). Failing closed on an unsupported filesystem is the sound
|
|
194
|
+
* resolution the reviewer required; hardlink support is effectively
|
|
195
|
+
* universal on the filesystems Node actually runs on.
|
|
196
|
+
*
|
|
197
|
+
* @param linkSyncOverride - @internal test seam (owned-lock.test.ts item 14)
|
|
198
|
+
* to exercise the hardlink-unavailable fail-closed path deterministically.
|
|
199
|
+
* Never set outside that test.
|
|
200
|
+
*/
|
|
201
|
+
export function createLockExclusive(path, recordJson, linkSyncOverride) {
|
|
202
|
+
const dir = dirname(path);
|
|
203
|
+
const tmp = writeTempClaimExclusive(dir, basename(path), recordJson);
|
|
204
|
+
const doLink = linkSyncOverride ?? linkSync;
|
|
205
|
+
try {
|
|
206
|
+
doLink(tmp, path);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const code = err.code;
|
|
211
|
+
if (code === 'EEXIST')
|
|
212
|
+
return false;
|
|
213
|
+
if (code && HARDLINK_UNAVAILABLE_CODES.has(code)) {
|
|
214
|
+
throw new Error(`[skillsmith] cannot create an owned lock at ${path}: this filesystem does not ` +
|
|
215
|
+
`support hardlinks (${code}), which the lock's atomic-and-content-complete creation ` +
|
|
216
|
+
`guarantee depends on. Failing closed rather than falling back to a non-atomic write ` +
|
|
217
|
+
`(which could leave a permanently unreclaimable lock on a crash mid-write).`);
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
try {
|
|
223
|
+
unlinkSync(tmp);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Already gone (nlink 2 -> 1 on a successful link), or never created.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Validate and destroy a stale main lock inside a region from which every
|
|
232
|
+
* other reclaimer is excluded (the reclaim lock). This is the ONLY code path
|
|
233
|
+
* that may ever unlink `<target>.lock` on the strength of a liveness
|
|
234
|
+
* inference: a caller's own pre-filter is advisory and discarded; only the
|
|
235
|
+
* re-read performed HERE, under the reclaim lock, authorizes the unlink.
|
|
236
|
+
*/
|
|
237
|
+
export function tryReclaimUnderLock(lockPath, reclaimPath, opts) {
|
|
238
|
+
const rToken = randomHex(8);
|
|
239
|
+
const rRecord = JSON.stringify({
|
|
240
|
+
v: 1,
|
|
241
|
+
pid: process.pid,
|
|
242
|
+
token: rToken,
|
|
243
|
+
host: hostname(),
|
|
244
|
+
acquiredAt: Date.now(),
|
|
245
|
+
}) + '\n';
|
|
246
|
+
const rDeadline = Date.now() + RECLAIM_LOCK_TIMEOUT_MS;
|
|
247
|
+
for (;;) {
|
|
248
|
+
if (createLockExclusive(reclaimPath, rRecord, opts.linkSyncOverride))
|
|
249
|
+
break;
|
|
250
|
+
if (Date.now() >= rDeadline)
|
|
251
|
+
return 'unavailable'; // concurrent reclaimer, or an R1 orphan
|
|
252
|
+
sleepSync(LOCK_RETRY_DELAY_MS);
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
// ===== RECLAIM CRITICAL SECTION -- mutually exclusive against every other reclaimer =====
|
|
256
|
+
const claim = readClaim(lockPath); // AUTHORITATIVE re-read
|
|
257
|
+
if (!opts.unsafeSkipRevalidation) {
|
|
258
|
+
if (claim.kind === 'absent')
|
|
259
|
+
return 'gone';
|
|
260
|
+
if (!isOwnerDefinitelyDead(claim))
|
|
261
|
+
return 'not-stale';
|
|
262
|
+
}
|
|
263
|
+
else if (claim.kind === 'absent') {
|
|
264
|
+
return 'gone'; // even the negative control cannot unlink something not there
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
unlinkSync(lockPath);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
if (err.code === 'ENOENT')
|
|
271
|
+
return 'gone';
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
return 'reclaimed';
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
releaseOwned(reclaimPath, rToken);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Release -- ownership-verified, idempotent
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
export function releaseOwned(path, token) {
|
|
284
|
+
const claim = readClaim(path);
|
|
285
|
+
if (claim.kind === 'v1' && claim.token === token) {
|
|
286
|
+
try {
|
|
287
|
+
unlinkSync(path);
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
if (err.code !== 'ENOENT') {
|
|
291
|
+
console.warn(`[skillsmith] lock_release_unlink_failed: could not remove ${path}: ${err.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
// We no longer own it -- unlinking would destroy a live holder's lock
|
|
297
|
+
// (it was reclaimed from us after we appeared dead, or hand-edited).
|
|
298
|
+
console.warn(`[skillsmith] lock_release_not_owner: ${path} is no longer owned by this process -- not removing it.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export function makeRelease(lockPath, token) {
|
|
302
|
+
let released = false;
|
|
303
|
+
return () => {
|
|
304
|
+
if (released)
|
|
305
|
+
return;
|
|
306
|
+
released = true;
|
|
307
|
+
releaseOwned(lockPath, token);
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=owned-lock.claim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owned-lock.claim.js","sourceRoot":"","sources":["../../../src/config/owned-lock.claim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,aAAa,GACd,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAClC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEnD,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,cAAc,EACd,uBAAuB,GACxB,MAAM,uBAAuB,CAAA;AAG9B,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AAC3C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAA;IACrD,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;AAC9B,CAAC;AAED,8EAA8E;AAC9E,4CAA4C;AAC5C,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;IAC3B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;IAExD,IAAI,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;QAC3B,IAAI,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAA;IAC/D,CAAC;IAED,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;IAChC,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;IACjF,MAAM,CAAC,GAAG,MAAiC,CAAA;IAC3C,IACE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;QACZ,OAAO,CAAC,CAAC,KAAK,CAAC,KAAK,QAAQ;QAC5B,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ;QAC9B,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,QAAQ;QAC7B,OAAO,CAAC,CAAC,YAAY,CAAC,KAAK,QAAQ,EACnC,CAAC;QACD,OAAO;YACL,IAAI,EAAE,IAAI;YACV,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC;YACb,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;YACjB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;YACf,UAAU,EAAE,CAAC,CAAC,YAAY,CAAC;SAC5B,CAAA;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,IAAI,EAAU,CAAA;IACd,IAAI,CAAC;QACH,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;IAC3B,CAAC;IACD,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,SAAS,CAAC,EAAE,CAAC,CAAA;QACxB,IAAI,EAAE,CAAC,IAAI,GAAG,cAAc;YAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;QAC5D,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,cAAc,CAAC,CAAA;QAC9C,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAA;QACjD,OAAO,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;IAChC,CAAC;YAAS,CAAC;QACT,SAAS,CAAC,EAAE,CAAC,CAAA;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB;IACnC,OAAO,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,KAAK,GAAG,CAAA;AAC/D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAY,EACZ,YAAiC,OAAO,CAAC,IAAI;IAE7C,IAAI,qBAAqB,EAAE;QAAE,OAAO,KAAK,CAAA;IACzC,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA,CAAC,mDAAmD;IACzF,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC;QAAE,OAAO,KAAK,CAAA;IAChE,IAAI,CAAC;QACH,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACvB,OAAO,KAAK,CAAA,CAAC,sBAAsB;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,IAAI,CAAA,CAAC,0BAA0B;QAC3F,OAAO,KAAK,CAAA,CAAC,8DAA8D;IAC7E,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAY;IAC1C,IAAI,qBAAqB,EAAE;QAAE,OAAO,kBAAkB,CAAA;IACtD,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC5C,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,aAAa,CAAA;IACjF,OAAO,MAAM,CAAA;AACf,CAAC;AAED,8EAA8E;AAC9E,0DAA0D;AAC1D,8EAA8E;AAE9E;;;;;;;;;;;GAWG;AACH,SAAS,uBAAuB,CAAC,GAAW,EAAE,IAAY,EAAE,UAAkB;IAC5E,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;QAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,IAAI,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;QACrD,IAAI,EAAU,CAAA;QACd,IAAI,CAAC;YACH,EAAE,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;QACjC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,SAAQ,CAAC,yCAAyC;YACxG,MAAM,GAAG,CAAA;QACX,CAAC;QACD,IAAI,CAAC;YACH,aAAa,CAAC,EAAE,EAAE,UAAU,CAAC,CAAA;QAC/B,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,EAAE,CAAC,CAAA;QACf,CAAC;QACD,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA,CAAC,mEAAmE;QACzF,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,MAAM,IAAI,KAAK,CACb,gEAAgE,GAAG,mBAAmB,CACvF,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,UAAkB,EAClB,gBAAkE;IAElE,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,MAAM,GAAG,GAAG,uBAAuB,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAA;IACpE,MAAM,MAAM,GAAG,gBAAgB,IAAI,QAAQ,CAAA;IAC3C,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACjB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,GAAI,GAA6B,CAAC,IAAI,CAAA;QAChD,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAA;QACnC,IAAI,IAAI,IAAI,0BAA0B,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CACb,+CAA+C,IAAI,6BAA6B;gBAC9E,sBAAsB,IAAI,2DAA2D;gBACrF,sFAAsF;gBACtF,4EAA4E,CAC/E,CAAA;QACH,CAAC;QACD,MAAM,GAAG,CAAA;IACX,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YACH,UAAU,CAAC,GAAG,CAAC,CAAA;QACjB,CAAC;QAAC,MAAM,CAAC;YACP,sEAAsE;QACxE,CAAC;IACH,CAAC;AACH,CAAC;AAYD;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAAgB,EAChB,WAAmB,EACnB,IAA4B;IAE5B,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,CAAA;IAC3B,MAAM,OAAO,GACX,IAAI,CAAC,SAAS,CAAC;QACb,CAAC,EAAE,CAAC;QACJ,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,KAAK,EAAE,MAAM;QACb,IAAI,EAAE,QAAQ,EAAE;QAChB,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;KACvB,CAAC,GAAG,IAAI,CAAA;IACX,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,uBAAuB,CAAA;IACtD,SAAS,CAAC;QACR,IAAI,mBAAmB,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,gBAAgB,CAAC;YAAE,MAAK;QAC3E,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,SAAS;YAAE,OAAO,aAAa,CAAA,CAAC,wCAAwC;QAC1F,SAAS,CAAC,mBAAmB,CAAC,CAAA;IAChC,CAAC;IACD,IAAI,CAAC;QACH,2FAA2F;QAC3F,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAA,CAAC,wBAAwB;QAC1D,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,CAAC;YACjC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,MAAM,CAAA;YAC1C,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC;gBAAE,OAAO,WAAW,CAAA;QACvD,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACnC,OAAO,MAAM,CAAA,CAAC,8DAA8D;QAC9E,CAAC;QACD,IAAI,CAAC;YACH,UAAU,CAAC,QAAQ,CAAC,CAAA;QACtB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,MAAM,CAAA;YACnE,MAAM,GAAG,CAAA;QACX,CAAC;QACD,OAAO,WAAW,CAAA;IACpB,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;IACnC,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,4CAA4C;AAC5C,8EAA8E;AAE9E,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,KAAa;IACtD,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC7B,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QACjD,IAAI,CAAC;YACH,UAAU,CAAC,IAAI,CAAC,CAAA;QAClB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrD,OAAO,CAAC,IAAI,CACV,6DAA6D,IAAI,KAAM,GAAa,CAAC,OAAO,EAAE,CAC/F,CAAA;YACH,CAAC;QACH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,sEAAsE;QACtE,qEAAqE;QACrE,OAAO,CAAC,IAAI,CACV,wCAAwC,IAAI,yDAAyD,CACtG,CAAA;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,QAAgB,EAAE,KAAa;IACzD,IAAI,QAAQ,GAAG,KAAK,CAAA;IACpB,OAAO,GAAG,EAAE;QACV,IAAI,QAAQ;YAAE,OAAM;QACpB,QAAQ,GAAG,IAAI,CAAA;QACf,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;IAC/B,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-level owned-lock primitive (SMI-5883 Wave 2, design revision 4).
|
|
3
|
+
* @module @skillsmith/core/config/owned-lock
|
|
4
|
+
*
|
|
5
|
+
* Replaces the single-level, age-based `acquireConfigLock` mechanism (which
|
|
6
|
+
* `config-atomic-write.ts` now implements as a thin wrapper over
|
|
7
|
+
* {@link acquireOwnedLock}). The single-level design was found unsound in
|
|
8
|
+
* round-3 review: it turned "I inspected this path and concluded the holder
|
|
9
|
+
* is dead" (computed in one region) into an unconditional
|
|
10
|
+
* `unlinkSync(lockPath)` (performed in another), with no mutual exclusion
|
|
11
|
+
* between the two. Any process that recreated the path in between had its
|
|
12
|
+
* LIVE lock destroyed by a contender acting on a stale decision.
|
|
13
|
+
*
|
|
14
|
+
* The general defect, stated so the fix can be checked against it: it is
|
|
15
|
+
* never sound to turn "I inspected this path and concluded X" into an
|
|
16
|
+
* unconditional destructive operation on that path, unless the interval
|
|
17
|
+
* between inspection and destruction excludes every actor that could change
|
|
18
|
+
* what the path holds. This module does not make the destruction
|
|
19
|
+
* conditional (POSIX offers no delete-if-inode primitive) -- it makes the
|
|
20
|
+
* interval exclusive, via a SECOND lock file (`<target>.lock.reclaim`,
|
|
21
|
+
* `owned-lock.claim.ts`'s `tryReclaimUnderLock`) that must be held before
|
|
22
|
+
* any reclaimer may re-read and then unlink `<target>.lock`. Every
|
|
23
|
+
* reclaimer therefore validates and destroys inside a region from which
|
|
24
|
+
* every other reclaimer -- and, by construction, every other mutator of
|
|
25
|
+
* that path -- is excluded. The authorization to reclaim can no longer go
|
|
26
|
+
* stale between being computed and being consumed, because nothing that
|
|
27
|
+
* could invalidate it is permitted to run in between.
|
|
28
|
+
*
|
|
29
|
+
* Two files, asymmetric policies:
|
|
30
|
+
* - `<target>.lock` -- the caller's lock. Auto-reclaimed, but ONLY
|
|
31
|
+
* from inside the reclaim critical section.
|
|
32
|
+
* - `<target>.lock.reclaim` -- serializes reclaim DECISIONS about the lock
|
|
33
|
+
* above. Strict, NEVER auto-reclaimed (a staleness heuristic here would
|
|
34
|
+
* reintroduce the identical duplicate-delete race one level up).
|
|
35
|
+
*
|
|
36
|
+
* This module is a thin wrapper over `owned-lock.acquire.ts`'s
|
|
37
|
+
* {@link acquireOwnedLockCore} -- it exists specifically so that the two
|
|
38
|
+
* destructive test-only seams on the core loop's options
|
|
39
|
+
* (`unsafeSkipReclaimRevalidation`, `linkSyncOverride`) are structurally
|
|
40
|
+
* unreachable from the public `@skillsmith/core/config/owned-lock` subpath
|
|
41
|
+
* export: this file's exported {@link AcquireOwnedLockOptions} simply does
|
|
42
|
+
* not have those fields, and this file never re-exports the internal
|
|
43
|
+
* `AcquireOwnedLockCoreOptions` type or `acquireOwnedLockCore` function
|
|
44
|
+
* that do. See owned-lock.acquire.ts's module comment for the full rationale.
|
|
45
|
+
*
|
|
46
|
+
* New-race audit (N1-N9) and residual risks (R1-R5) are documented in the
|
|
47
|
+
* SMI-5883 design doc; the short version: R1 (`SIGKILL` inside the
|
|
48
|
+
* ~3-syscall reclaim critical section orphans the reclaim lock) is accepted
|
|
49
|
+
* and loudly diagnosable ({@link StuckLockError} names both files + the
|
|
50
|
+
* manual unstick procedure below); legacy (pre-v1, bare-decimal-PID) claims
|
|
51
|
+
* and unparseable claims are NEVER auto-reclaimed (D-5/D-6) because they
|
|
52
|
+
* carry no `host` field, so "dead on this host" cannot establish "dead" for
|
|
53
|
+
* a claim that might belong to a process on a different host sharing a
|
|
54
|
+
* networked `$HOME`.
|
|
55
|
+
*
|
|
56
|
+
* Manual unstick (required whenever a `StuckLockError` fires with a reason
|
|
57
|
+
* other than `held`):
|
|
58
|
+
* 1. Confirm no skillsmith process is running:
|
|
59
|
+
* ps -ax | grep -E '[s]killsmith|[s]klx'
|
|
60
|
+
* 2. Inspect the claim (read-only, safe):
|
|
61
|
+
* cat <lockPath>
|
|
62
|
+
* cat <reclaimPath> # only if the error named it
|
|
63
|
+
* 3. Remove ONLY the file(s) the error named:
|
|
64
|
+
* rm <lockPath>
|
|
65
|
+
* rm <reclaimPath>
|
|
66
|
+
*
|
|
67
|
+
* Opt-out: `SKILLSMITH_LOCK_NO_AUTO_RECLAIM=1` disables auto-reclaim
|
|
68
|
+
* entirely (degrades to the strict, no-reclaim design) -- a sound escape
|
|
69
|
+
* hatch for R5 (an `os.hostname()` collision across two containers with
|
|
70
|
+
* distinct PID namespaces bind-mounting the same `$HOME`). Registered in
|
|
71
|
+
* `docs/internal/process/guards-and-opt-outs.md`.
|
|
72
|
+
*/
|
|
73
|
+
import { StuckLockError } from './owned-lock.acquire.js';
|
|
74
|
+
import type { AcquireOwnedLockOptions } from './owned-lock.types.js';
|
|
75
|
+
export type { AcquireOwnedLockOptions, StuckLockReason } from './owned-lock.types.js';
|
|
76
|
+
export { LOCK_ACQUIRE_TIMEOUT_MS, LOCK_RETRY_DELAY_MS, MAX_LOCK_BYTES, RECLAIM_LOCK_TIMEOUT_MS, RECLAIM_PROBE_AFTER_MS, RECLAIM_PROBE_INTERVAL_MS, } from './owned-lock.types.js';
|
|
77
|
+
export { StuckLockError };
|
|
78
|
+
/**
|
|
79
|
+
* Acquire an exclusive, cross-process, OWNED lock guarding `target`. Unlike
|
|
80
|
+
* a plain create-exclusive lock, ownership is verified on release (a token,
|
|
81
|
+
* not just presence) and staleness is verified by PROCESS LIVENESS, not file
|
|
82
|
+
* age -- and reclaimed only from inside a second, strict-no-auto-reclaim
|
|
83
|
+
* lock (`<target>.lock.reclaim`) that serializes reclaim decisions. See the
|
|
84
|
+
* module docstring for the full soundness argument.
|
|
85
|
+
*
|
|
86
|
+
* @param target - Path being guarded (NOT the lock file itself -- the lock
|
|
87
|
+
* file is `${target}.lock`, the reclaim lock `${target}.lock.reclaim`).
|
|
88
|
+
* @returns A release function. Callers MUST call it exactly once, in a
|
|
89
|
+
* `finally` block. Idempotent -- a second call is a no-op.
|
|
90
|
+
*
|
|
91
|
+
* SMI-5883 code-review round 2: `opts` is destructured field-by-field into a
|
|
92
|
+
* FRESH object rather than forwarded as-is. TypeScript's excess-property
|
|
93
|
+
* check only fires on an object LITERAL passed directly at the call site --
|
|
94
|
+
* a caller passing a variable, a cast, or plain JavaScript (no type checking
|
|
95
|
+
* at all) is not constrained by `AcquireOwnedLockOptions` omitting the two
|
|
96
|
+
* unsafe fields, and `acquireOwnedLockCore` reads them by property name at
|
|
97
|
+
* runtime. Rebuilding the object here means an injected
|
|
98
|
+
* `unsafeSkipReclaimRevalidation`/`linkSyncOverride` on the caller's `opts`
|
|
99
|
+
* is structurally dropped before it ever reaches the core loop, regardless
|
|
100
|
+
* of what extra properties `opts` carries.
|
|
101
|
+
*
|
|
102
|
+
* SMI-5883 code-review round 3: the two unsafe fields are ALSO set to
|
|
103
|
+
* `undefined` explicitly, as OWN properties of the fresh object, rather than
|
|
104
|
+
* simply omitted. A plain `{ ...safeFields }` object still inherits from
|
|
105
|
+
* `Object.prototype` -- if that prototype were ever globally polluted (a
|
|
106
|
+
* distinct, severe vulnerability class in its own right, reachable only by
|
|
107
|
+
* an attacker who already has arbitrary code execution in this process),
|
|
108
|
+
* ordinary property access on an object with no OWN `unsafeSkipReclaimRevalidation`/
|
|
109
|
+
* `linkSyncOverride` would still resolve them via the prototype chain. An
|
|
110
|
+
* explicit own-property `undefined` shadows any such inherited value.
|
|
111
|
+
*/
|
|
112
|
+
export declare function acquireOwnedLock(target: string, opts?: AcquireOwnedLockOptions): () => void;
|
|
113
|
+
//# sourceMappingURL=owned-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owned-lock.d.ts","sourceRoot":"","sources":["../../../src/config/owned-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH,OAAO,EAAwB,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAC9E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AAEpE,YAAY,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACrF,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,cAAc,EAAE,CAAA;AAEzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,uBAA4B,GAAG,MAAM,IAAI,CAU/F"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-level owned-lock primitive (SMI-5883 Wave 2, design revision 4).
|
|
3
|
+
* @module @skillsmith/core/config/owned-lock
|
|
4
|
+
*
|
|
5
|
+
* Replaces the single-level, age-based `acquireConfigLock` mechanism (which
|
|
6
|
+
* `config-atomic-write.ts` now implements as a thin wrapper over
|
|
7
|
+
* {@link acquireOwnedLock}). The single-level design was found unsound in
|
|
8
|
+
* round-3 review: it turned "I inspected this path and concluded the holder
|
|
9
|
+
* is dead" (computed in one region) into an unconditional
|
|
10
|
+
* `unlinkSync(lockPath)` (performed in another), with no mutual exclusion
|
|
11
|
+
* between the two. Any process that recreated the path in between had its
|
|
12
|
+
* LIVE lock destroyed by a contender acting on a stale decision.
|
|
13
|
+
*
|
|
14
|
+
* The general defect, stated so the fix can be checked against it: it is
|
|
15
|
+
* never sound to turn "I inspected this path and concluded X" into an
|
|
16
|
+
* unconditional destructive operation on that path, unless the interval
|
|
17
|
+
* between inspection and destruction excludes every actor that could change
|
|
18
|
+
* what the path holds. This module does not make the destruction
|
|
19
|
+
* conditional (POSIX offers no delete-if-inode primitive) -- it makes the
|
|
20
|
+
* interval exclusive, via a SECOND lock file (`<target>.lock.reclaim`,
|
|
21
|
+
* `owned-lock.claim.ts`'s `tryReclaimUnderLock`) that must be held before
|
|
22
|
+
* any reclaimer may re-read and then unlink `<target>.lock`. Every
|
|
23
|
+
* reclaimer therefore validates and destroys inside a region from which
|
|
24
|
+
* every other reclaimer -- and, by construction, every other mutator of
|
|
25
|
+
* that path -- is excluded. The authorization to reclaim can no longer go
|
|
26
|
+
* stale between being computed and being consumed, because nothing that
|
|
27
|
+
* could invalidate it is permitted to run in between.
|
|
28
|
+
*
|
|
29
|
+
* Two files, asymmetric policies:
|
|
30
|
+
* - `<target>.lock` -- the caller's lock. Auto-reclaimed, but ONLY
|
|
31
|
+
* from inside the reclaim critical section.
|
|
32
|
+
* - `<target>.lock.reclaim` -- serializes reclaim DECISIONS about the lock
|
|
33
|
+
* above. Strict, NEVER auto-reclaimed (a staleness heuristic here would
|
|
34
|
+
* reintroduce the identical duplicate-delete race one level up).
|
|
35
|
+
*
|
|
36
|
+
* This module is a thin wrapper over `owned-lock.acquire.ts`'s
|
|
37
|
+
* {@link acquireOwnedLockCore} -- it exists specifically so that the two
|
|
38
|
+
* destructive test-only seams on the core loop's options
|
|
39
|
+
* (`unsafeSkipReclaimRevalidation`, `linkSyncOverride`) are structurally
|
|
40
|
+
* unreachable from the public `@skillsmith/core/config/owned-lock` subpath
|
|
41
|
+
* export: this file's exported {@link AcquireOwnedLockOptions} simply does
|
|
42
|
+
* not have those fields, and this file never re-exports the internal
|
|
43
|
+
* `AcquireOwnedLockCoreOptions` type or `acquireOwnedLockCore` function
|
|
44
|
+
* that do. See owned-lock.acquire.ts's module comment for the full rationale.
|
|
45
|
+
*
|
|
46
|
+
* New-race audit (N1-N9) and residual risks (R1-R5) are documented in the
|
|
47
|
+
* SMI-5883 design doc; the short version: R1 (`SIGKILL` inside the
|
|
48
|
+
* ~3-syscall reclaim critical section orphans the reclaim lock) is accepted
|
|
49
|
+
* and loudly diagnosable ({@link StuckLockError} names both files + the
|
|
50
|
+
* manual unstick procedure below); legacy (pre-v1, bare-decimal-PID) claims
|
|
51
|
+
* and unparseable claims are NEVER auto-reclaimed (D-5/D-6) because they
|
|
52
|
+
* carry no `host` field, so "dead on this host" cannot establish "dead" for
|
|
53
|
+
* a claim that might belong to a process on a different host sharing a
|
|
54
|
+
* networked `$HOME`.
|
|
55
|
+
*
|
|
56
|
+
* Manual unstick (required whenever a `StuckLockError` fires with a reason
|
|
57
|
+
* other than `held`):
|
|
58
|
+
* 1. Confirm no skillsmith process is running:
|
|
59
|
+
* ps -ax | grep -E '[s]killsmith|[s]klx'
|
|
60
|
+
* 2. Inspect the claim (read-only, safe):
|
|
61
|
+
* cat <lockPath>
|
|
62
|
+
* cat <reclaimPath> # only if the error named it
|
|
63
|
+
* 3. Remove ONLY the file(s) the error named:
|
|
64
|
+
* rm <lockPath>
|
|
65
|
+
* rm <reclaimPath>
|
|
66
|
+
*
|
|
67
|
+
* Opt-out: `SKILLSMITH_LOCK_NO_AUTO_RECLAIM=1` disables auto-reclaim
|
|
68
|
+
* entirely (degrades to the strict, no-reclaim design) -- a sound escape
|
|
69
|
+
* hatch for R5 (an `os.hostname()` collision across two containers with
|
|
70
|
+
* distinct PID namespaces bind-mounting the same `$HOME`). Registered in
|
|
71
|
+
* `docs/internal/process/guards-and-opt-outs.md`.
|
|
72
|
+
*/
|
|
73
|
+
import { acquireOwnedLockCore, StuckLockError } from './owned-lock.acquire.js';
|
|
74
|
+
export { LOCK_ACQUIRE_TIMEOUT_MS, LOCK_RETRY_DELAY_MS, MAX_LOCK_BYTES, RECLAIM_LOCK_TIMEOUT_MS, RECLAIM_PROBE_AFTER_MS, RECLAIM_PROBE_INTERVAL_MS, } from './owned-lock.types.js';
|
|
75
|
+
export { StuckLockError };
|
|
76
|
+
/**
|
|
77
|
+
* Acquire an exclusive, cross-process, OWNED lock guarding `target`. Unlike
|
|
78
|
+
* a plain create-exclusive lock, ownership is verified on release (a token,
|
|
79
|
+
* not just presence) and staleness is verified by PROCESS LIVENESS, not file
|
|
80
|
+
* age -- and reclaimed only from inside a second, strict-no-auto-reclaim
|
|
81
|
+
* lock (`<target>.lock.reclaim`) that serializes reclaim decisions. See the
|
|
82
|
+
* module docstring for the full soundness argument.
|
|
83
|
+
*
|
|
84
|
+
* @param target - Path being guarded (NOT the lock file itself -- the lock
|
|
85
|
+
* file is `${target}.lock`, the reclaim lock `${target}.lock.reclaim`).
|
|
86
|
+
* @returns A release function. Callers MUST call it exactly once, in a
|
|
87
|
+
* `finally` block. Idempotent -- a second call is a no-op.
|
|
88
|
+
*
|
|
89
|
+
* SMI-5883 code-review round 2: `opts` is destructured field-by-field into a
|
|
90
|
+
* FRESH object rather than forwarded as-is. TypeScript's excess-property
|
|
91
|
+
* check only fires on an object LITERAL passed directly at the call site --
|
|
92
|
+
* a caller passing a variable, a cast, or plain JavaScript (no type checking
|
|
93
|
+
* at all) is not constrained by `AcquireOwnedLockOptions` omitting the two
|
|
94
|
+
* unsafe fields, and `acquireOwnedLockCore` reads them by property name at
|
|
95
|
+
* runtime. Rebuilding the object here means an injected
|
|
96
|
+
* `unsafeSkipReclaimRevalidation`/`linkSyncOverride` on the caller's `opts`
|
|
97
|
+
* is structurally dropped before it ever reaches the core loop, regardless
|
|
98
|
+
* of what extra properties `opts` carries.
|
|
99
|
+
*
|
|
100
|
+
* SMI-5883 code-review round 3: the two unsafe fields are ALSO set to
|
|
101
|
+
* `undefined` explicitly, as OWN properties of the fresh object, rather than
|
|
102
|
+
* simply omitted. A plain `{ ...safeFields }` object still inherits from
|
|
103
|
+
* `Object.prototype` -- if that prototype were ever globally polluted (a
|
|
104
|
+
* distinct, severe vulnerability class in its own right, reachable only by
|
|
105
|
+
* an attacker who already has arbitrary code execution in this process),
|
|
106
|
+
* ordinary property access on an object with no OWN `unsafeSkipReclaimRevalidation`/
|
|
107
|
+
* `linkSyncOverride` would still resolve them via the prototype chain. An
|
|
108
|
+
* explicit own-property `undefined` shadows any such inherited value.
|
|
109
|
+
*/
|
|
110
|
+
export function acquireOwnedLock(target, opts = {}) {
|
|
111
|
+
return acquireOwnedLockCore(target, {
|
|
112
|
+
timeoutMs: opts.timeoutMs,
|
|
113
|
+
label: opts.label,
|
|
114
|
+
reclaimProbeAfterMs: opts.reclaimProbeAfterMs,
|
|
115
|
+
onReclaimBoundary: opts.onReclaimBoundary,
|
|
116
|
+
onReclaimOutcome: opts.onReclaimOutcome,
|
|
117
|
+
unsafeSkipReclaimRevalidation: undefined,
|
|
118
|
+
linkSyncOverride: undefined,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=owned-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owned-lock.js","sourceRoot":"","sources":["../../../src/config/owned-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAI9E,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,cAAc,EAAE,CAAA;AAEzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,OAAgC,EAAE;IACjF,OAAO,oBAAoB,CAAC,MAAM,EAAE;QAClC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,mBAAmB,EAAE,IAAI,CAAC,mBAAmB;QAC7C,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;QACzC,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;QACvC,6BAA6B,EAAE,SAAS;QACxC,gBAAgB,EAAE,SAAS;KAC5B,CAAC,CAAA;AACJ,CAAC"}
|