contextos-agents 1.7.0 → 2.0.0-beta.2
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/.agents/AGENTS.md +1 -1
- package/.agents/adapters/aider/export.js +117 -99
- package/.agents/adapters/claude/export.js +68 -26
- package/.agents/adapters/copilot/export.js +90 -53
- package/.agents/adapters/cursor/export.js +80 -101
- package/.agents/adapters/drift-detector.js +196 -0
- package/.agents/adapters/gemini/export.js +76 -45
- package/.agents/adapters/pure-compiler.js +443 -0
- package/.agents/adapters/zed/export.js +104 -96
- package/.agents/compiled/registry.v2.json +504 -0
- package/.agents/compiled/registry.v2.sha256 +1 -0
- package/.agents/compiler/manifest-compiler.js +963 -0
- package/.agents/compiler/vendor/yaml.LICENSE.txt +13 -0
- package/.agents/compiler/vendor/yaml.SBOM.json +6 -0
- package/.agents/compiler/vendor/yaml.js +139 -0
- package/.agents/core/profiles/init.yaml +25 -0
- package/.agents/core/skills/context-manager/skill.yaml +3 -5
- package/.agents/core/skills/context-os/SKILL.md +3 -6
- package/.agents/core/skills/context-os/skill.yaml +3 -8
- package/.agents/core/skills/engineering-workflow/skill.yaml +1 -7
- package/.agents/core/skills/gemini-precision/skill.yaml +1 -6
- package/.agents/core/skills/gstack-roles/skill.yaml +3 -6
- package/.agents/core/skills/ponytail-mindset/skill.yaml +1 -7
- package/.agents/core/skills/security/skill.yaml +15 -3
- package/.agents/ctx.js +574 -111
- package/.agents/customization-dx.js +282 -0
- package/.agents/doctor.js +719 -64
- package/.agents/filesystem/index.js +71 -0
- package/.agents/filesystem/journaled-transaction.js +451 -0
- package/.agents/filesystem/lockfile-v2.js +275 -0
- package/.agents/filesystem/platform-hardening.js +222 -0
- package/.agents/filesystem/project-lock.js +218 -0
- package/.agents/filesystem/safe-path.js +256 -0
- package/.agents/generated/claude/skills/context-os/SKILL.md +1 -1
- package/.agents/generated/gemini/skills/context-os/SKILL.md +2 -2
- package/.agents/plugins/contextos/plugin.json +1 -1
- package/.agents/plugins.js +259 -60
- package/.agents/profiles.js +486 -51
- package/.agents/resolver.js +50 -534
- package/.agents/schemas/attestation.review.v1.json +111 -0
- package/.agents/schemas/attestation.verification.v1.json +85 -0
- package/.agents/schemas/lockfile.v2.schema.json +134 -0
- package/.agents/schemas/profile.v2.schema.json +114 -0
- package/.agents/schemas/runtime.thread.v1.json +192 -0
- package/.agents/schemas/skill.manifest.v2.json +177 -0
- package/.agents/schemas/verification.spec.v1.json +39 -0
- package/.agents/schemas/workspace.graph.schema.json +106 -0
- package/.agents/transaction-core/event-store.js +288 -0
- package/.agents/transaction-core/idempotency.js +129 -0
- package/.agents/transaction-core/ipc-lock.js +311 -0
- package/.agents/transaction-core/plugin-supply-chain-bundle.js +436 -0
- package/.agents/validate.js +44 -1
- package/.agents/watch.js +354 -102
- package/.agents/workspace/workspace-graph.js +778 -0
- package/README.md +59 -387
- package/benchmarks/v2/analysis/statistics.js +140 -0
- package/benchmarks/v2/analysis/stats.js +69 -0
- package/benchmarks/v2/arms/arm-definitions.js +79 -0
- package/benchmarks/v2/dataset.schema.json +34 -0
- package/benchmarks/v2/evaluators/index.js +25 -0
- package/benchmarks/v2/evaluators/verified-success.js +116 -0
- package/benchmarks/v2/harness/runner.js +88 -0
- package/benchmarks/v2/pilot-tasks.json +392 -0
- package/bin/commands/recover.js +88 -0
- package/bin/commands/update.js +80 -17
- package/bin/commands.js +62 -25
- package/bin/index.js +138 -81
- package/bin/lib/lockfile.js +5 -3
- package/bin/lib/safe-writer.js +34 -3
- package/package.json +85 -72
- package/registry.json +2 -2
- package/registry.v2.schema.json +86 -0
- package/.agents/core/profiles/backend.yaml +0 -47
- package/.agents/core/profiles/enterprise.yaml +0 -46
- package/.agents/core/profiles/frontend.yaml +0 -46
- package/.agents/core/profiles/hackathon.yaml +0 -45
- package/.agents/core/profiles/mvp.yaml +0 -44
- package/.agents/core/profiles/startup.yaml +0 -48
- package/.agents/core/skills/adapters/EXAMPLES.md +0 -19
- package/.agents/core/skills/adapters/SKILL.md +0 -105
- package/.agents/core/skills/adapters/TROUBLESHOOTING.md +0 -7
- package/.agents/core/skills/adapters/VALIDATION.json +0 -12
- package/.agents/core/skills/adapters/skill.yaml +0 -16
- package/.agents/core/skills/architecture-diagrams/SKILL.md +0 -108
- package/.agents/core/skills/architecture-diagrams/VALIDATION.json +0 -12
- package/.agents/core/skills/architecture-diagrams/skill.yaml +0 -12
- package/.agents/core/skills/brutalist-design/SKILL.md +0 -150
- package/.agents/core/skills/brutalist-design/VALIDATION.json +0 -12
- package/.agents/core/skills/brutalist-design/skill.yaml +0 -12
- package/.agents/core/skills/database/EXAMPLES.md +0 -74
- package/.agents/core/skills/database/SKILL.md +0 -101
- package/.agents/core/skills/database/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/database/VALIDATION.json +0 -11
- package/.agents/core/skills/database/skill.yaml +0 -31
- package/.agents/core/skills/ddd/EXAMPLES.md +0 -42
- package/.agents/core/skills/ddd/SKILL.md +0 -247
- package/.agents/core/skills/ddd/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ddd/VALIDATION.json +0 -12
- package/.agents/core/skills/ddd/ddd.md +0 -178
- package/.agents/core/skills/ddd/skill.yaml +0 -17
- package/.agents/core/skills/decisions/EXAMPLES.md +0 -35
- package/.agents/core/skills/decisions/SKILL.md +0 -90
- package/.agents/core/skills/decisions/TROUBLESHOOTING.md +0 -13
- package/.agents/core/skills/decisions/VALIDATION.json +0 -12
- package/.agents/core/skills/decisions/skill.yaml +0 -16
- package/.agents/core/skills/docker/EXAMPLES.md +0 -56
- package/.agents/core/skills/docker/SKILL.md +0 -63
- package/.agents/core/skills/docker/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/docker/VALIDATION.json +0 -11
- package/.agents/core/skills/docker/skill.yaml +0 -29
- package/.agents/core/skills/fastapi/EXAMPLES.md +0 -36
- package/.agents/core/skills/fastapi/SKILL.md +0 -148
- package/.agents/core/skills/fastapi/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/fastapi/VALIDATION.json +0 -12
- package/.agents/core/skills/fastapi/fastapi.md +0 -112
- package/.agents/core/skills/fastapi/skill.yaml +0 -17
- package/.agents/core/skills/generators/EXAMPLES.md +0 -19
- package/.agents/core/skills/generators/SKILL.md +0 -112
- package/.agents/core/skills/generators/TROUBLESHOOTING.md +0 -7
- package/.agents/core/skills/generators/VALIDATION.json +0 -12
- package/.agents/core/skills/generators/skill.yaml +0 -25
- package/.agents/core/skills/generators/templates/API.md +0 -77
- package/.agents/core/skills/generators/templates/ARCHITECTURE.md +0 -70
- package/.agents/core/skills/generators/templates/DATABASE.md +0 -42
- package/.agents/core/skills/generators/templates/DECISION.md +0 -46
- package/.agents/core/skills/generators/templates/PRD.md +0 -67
- package/.agents/core/skills/generators/templates/PROJECT_GRAPH.md +0 -56
- package/.agents/core/skills/generators/templates/ROADMAP.md +0 -51
- package/.agents/core/skills/generators/templates/TASKS.md +0 -43
- package/.agents/core/skills/generators/templates/UI.md +0 -73
- package/.agents/core/skills/graphify/EXAMPLES.md +0 -73
- package/.agents/core/skills/graphify/SKILL.md +0 -130
- package/.agents/core/skills/graphify/VALIDATION.json +0 -12
- package/.agents/core/skills/graphify/skill.yaml +0 -18
- package/.agents/core/skills/impeccable-design/EXAMPLES.md +0 -26
- package/.agents/core/skills/impeccable-design/SKILL.md +0 -201
- package/.agents/core/skills/impeccable-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/impeccable-design/VALIDATION.json +0 -12
- package/.agents/core/skills/impeccable-design/skill.yaml +0 -20
- package/.agents/core/skills/interview-me/SKILL.md +0 -97
- package/.agents/core/skills/interview-me/VALIDATION.json +0 -12
- package/.agents/core/skills/interview-me/skill.yaml +0 -12
- package/.agents/core/skills/microservices/EXAMPLES.md +0 -38
- package/.agents/core/skills/microservices/SKILL.md +0 -164
- package/.agents/core/skills/microservices/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/microservices/VALIDATION.json +0 -12
- package/.agents/core/skills/microservices/microservices.md +0 -119
- package/.agents/core/skills/microservices/skill.yaml +0 -17
- package/.agents/core/skills/minimalist-design/SKILL.md +0 -113
- package/.agents/core/skills/minimalist-design/VALIDATION.json +0 -12
- package/.agents/core/skills/minimalist-design/skill.yaml +0 -12
- package/.agents/core/skills/nestjs/EXAMPLES.md +0 -40
- package/.agents/core/skills/nestjs/SKILL.md +0 -139
- package/.agents/core/skills/nestjs/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/nestjs/VALIDATION.json +0 -12
- package/.agents/core/skills/nestjs/nestjs.md +0 -103
- package/.agents/core/skills/nestjs/skill.yaml +0 -17
- package/.agents/core/skills/nextjs/EXAMPLES.md +0 -40
- package/.agents/core/skills/nextjs/SKILL.md +0 -163
- package/.agents/core/skills/nextjs/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/nextjs/VALIDATION.json +0 -12
- package/.agents/core/skills/nextjs/nextjs.md +0 -67
- package/.agents/core/skills/nextjs/skill.yaml +0 -17
- package/.agents/core/skills/node/EXAMPLES.md +0 -80
- package/.agents/core/skills/node/SKILL.md +0 -128
- package/.agents/core/skills/node/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/node/VALIDATION.json +0 -12
- package/.agents/core/skills/node/node.md +0 -87
- package/.agents/core/skills/node/skill.yaml +0 -17
- package/.agents/core/skills/performance/EXAMPLES.md +0 -30
- package/.agents/core/skills/performance/SKILL.md +0 -75
- package/.agents/core/skills/performance/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/performance/VALIDATION.json +0 -12
- package/.agents/core/skills/performance/performance.md +0 -52
- package/.agents/core/skills/performance/skill.yaml +0 -17
- package/.agents/core/skills/react/EXAMPLES.md +0 -79
- package/.agents/core/skills/react/SKILL.md +0 -132
- package/.agents/core/skills/react/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/react/VALIDATION.json +0 -12
- package/.agents/core/skills/react/react.md +0 -93
- package/.agents/core/skills/react/skill.yaml +0 -17
- package/.agents/core/skills/react-best-practices/SKILL.md +0 -155
- package/.agents/core/skills/react-best-practices/VALIDATION.json +0 -12
- package/.agents/core/skills/react-best-practices/skill.yaml +0 -14
- package/.agents/core/skills/redesign-audit/SKILL.md +0 -117
- package/.agents/core/skills/redesign-audit/VALIDATION.json +0 -12
- package/.agents/core/skills/redesign-audit/skill.yaml +0 -12
- package/.agents/core/skills/soft-design/SKILL.md +0 -108
- package/.agents/core/skills/soft-design/VALIDATION.json +0 -12
- package/.agents/core/skills/soft-design/skill.yaml +0 -12
- package/.agents/core/skills/state-management/EXAMPLES.md +0 -56
- package/.agents/core/skills/state-management/SKILL.md +0 -48
- package/.agents/core/skills/state-management/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/state-management/VALIDATION.json +0 -11
- package/.agents/core/skills/state-management/skill.yaml +0 -28
- package/.agents/core/skills/subagent-orchestrator/SKILL.md +0 -117
- package/.agents/core/skills/subagent-orchestrator/VALIDATION.json +0 -12
- package/.agents/core/skills/subagent-orchestrator/skill.yaml +0 -12
- package/.agents/core/skills/system-design/EXAMPLES.md +0 -75
- package/.agents/core/skills/system-design/SKILL.md +0 -419
- package/.agents/core/skills/system-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/system-design/VALIDATION.json +0 -12
- package/.agents/core/skills/system-design/skill.yaml +0 -20
- package/.agents/core/skills/system-design/system-design.md +0 -112
- package/.agents/core/skills/testing/EXAMPLES.md +0 -71
- package/.agents/core/skills/testing/SKILL.md +0 -70
- package/.agents/core/skills/testing/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/testing/VALIDATION.json +0 -11
- package/.agents/core/skills/testing/skill.yaml +0 -32
- package/.agents/core/skills/typescript/EXAMPLES.md +0 -64
- package/.agents/core/skills/typescript/SKILL.md +0 -112
- package/.agents/core/skills/typescript/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/typescript/VALIDATION.json +0 -12
- package/.agents/core/skills/typescript/skill.yaml +0 -17
- package/.agents/core/skills/typescript/typescript.md +0 -71
- package/.agents/core/skills/ui-design/EXAMPLES.md +0 -21
- package/.agents/core/skills/ui-design/SKILL.md +0 -124
- package/.agents/core/skills/ui-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ui-design/VALIDATION.json +0 -12
- package/.agents/core/skills/ui-design/skill.yaml +0 -17
- package/.agents/core/skills/ui-design/ui.md +0 -88
- package/.agents/core/skills/ui-ux-pro/EXAMPLES.md +0 -62
- package/.agents/core/skills/ui-ux-pro/SKILL.md +0 -375
- package/.agents/core/skills/ui-ux-pro/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ui-ux-pro/VALIDATION.json +0 -12
- package/.agents/core/skills/ui-ux-pro/skill.yaml +0 -19
- package/.agents/core/skills/ux-design/EXAMPLES.md +0 -36
- package/.agents/core/skills/ux-design/SKILL.md +0 -116
- package/.agents/core/skills/ux-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ux-design/VALIDATION.json +0 -12
- package/.agents/core/skills/ux-design/skill.yaml +0 -17
- package/.agents/core/skills/ux-design/ux.md +0 -80
- package/.agents/core/skills/vercel-optimize/SKILL.md +0 -83
- package/.agents/core/skills/vercel-optimize/VALIDATION.json +0 -12
- package/.agents/core/skills/vercel-optimize/scripts/collect-signals.mjs +0 -131
- package/.agents/core/skills/vercel-optimize/scripts/gate-investigations.mjs +0 -142
- package/.agents/core/skills/vercel-optimize/scripts/merge-signals.mjs +0 -143
- package/.agents/core/skills/vercel-optimize/scripts/scan-codebase.mjs +0 -174
- package/.agents/core/skills/vercel-optimize/skill.yaml +0 -18
- package/.agents/core/skills/web-accessibility/EXAMPLES.md +0 -39
- package/.agents/core/skills/web-accessibility/SKILL.md +0 -170
- package/.agents/core/skills/web-accessibility/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/web-accessibility/VALIDATION.json +0 -12
- package/.agents/core/skills/web-accessibility/accessibility.md +0 -63
- package/.agents/core/skills/web-accessibility/skill.yaml +0 -17
- package/.agents/generated/claude/skills/adapters/SKILL.md +0 -126
- package/.agents/generated/claude/skills/architecture-diagrams/SKILL.md +0 -101
- package/.agents/generated/claude/skills/brutalist-design/SKILL.md +0 -145
- package/.agents/generated/claude/skills/database/SKILL.md +0 -191
- package/.agents/generated/claude/skills/ddd/SKILL.md +0 -305
- package/.agents/generated/claude/skills/decisions/SKILL.md +0 -134
- package/.agents/generated/claude/skills/docker/SKILL.md +0 -135
- package/.agents/generated/claude/skills/fastapi/SKILL.md +0 -200
- package/.agents/generated/claude/skills/generators/SKILL.md +0 -133
- package/.agents/generated/claude/skills/graphify/SKILL.md +0 -198
- package/.agents/generated/claude/skills/impeccable-design/SKILL.md +0 -241
- package/.agents/generated/claude/skills/interview-me/SKILL.md +0 -90
- package/.agents/generated/claude/skills/microservices/SKILL.md +0 -218
- package/.agents/generated/claude/skills/minimalist-design/SKILL.md +0 -108
- package/.agents/generated/claude/skills/nestjs/SKILL.md +0 -195
- package/.agents/generated/claude/skills/nextjs/SKILL.md +0 -219
- package/.agents/generated/claude/skills/node/SKILL.md +0 -224
- package/.agents/generated/claude/skills/performance/SKILL.md +0 -121
- package/.agents/generated/claude/skills/react/SKILL.md +0 -227
- package/.agents/generated/claude/skills/react-best-practices/SKILL.md +0 -146
- package/.agents/generated/claude/skills/redesign-audit/SKILL.md +0 -112
- package/.agents/generated/claude/skills/soft-design/SKILL.md +0 -103
- package/.agents/generated/claude/skills/state-management/SKILL.md +0 -120
- package/.agents/generated/claude/skills/subagent-orchestrator/SKILL.md +0 -110
- package/.agents/generated/claude/skills/system-design/SKILL.md +0 -507
- package/.agents/generated/claude/skills/testing/SKILL.md +0 -157
- package/.agents/generated/claude/skills/typescript/SKILL.md +0 -192
- package/.agents/generated/claude/skills/ui-design/SKILL.md +0 -161
- package/.agents/generated/claude/skills/ui-ux-pro/SKILL.md +0 -451
- package/.agents/generated/claude/skills/ux-design/SKILL.md +0 -168
- package/.agents/generated/claude/skills/vercel-optimize/SKILL.md +0 -76
- package/.agents/generated/claude/skills/web-accessibility/SKILL.md +0 -225
- package/.agents/generated/gemini/skills/adapters/SKILL.md +0 -135
- package/.agents/generated/gemini/skills/architecture-diagrams/SKILL.md +0 -107
- package/.agents/generated/gemini/skills/brutalist-design/SKILL.md +0 -151
- package/.agents/generated/gemini/skills/database/SKILL.md +0 -200
- package/.agents/generated/gemini/skills/ddd/SKILL.md +0 -314
- package/.agents/generated/gemini/skills/decisions/SKILL.md +0 -143
- package/.agents/generated/gemini/skills/docker/SKILL.md +0 -144
- package/.agents/generated/gemini/skills/fastapi/SKILL.md +0 -209
- package/.agents/generated/gemini/skills/generators/SKILL.md +0 -142
- package/.agents/generated/gemini/skills/graphify/SKILL.md +0 -205
- package/.agents/generated/gemini/skills/impeccable-design/SKILL.md +0 -250
- package/.agents/generated/gemini/skills/interview-me/SKILL.md +0 -96
- package/.agents/generated/gemini/skills/microservices/SKILL.md +0 -227
- package/.agents/generated/gemini/skills/minimalist-design/SKILL.md +0 -114
- package/.agents/generated/gemini/skills/nestjs/SKILL.md +0 -204
- package/.agents/generated/gemini/skills/nextjs/SKILL.md +0 -298
- package/.agents/generated/gemini/skills/node/SKILL.md +0 -323
- package/.agents/generated/gemini/skills/performance/SKILL.md +0 -185
- package/.agents/generated/gemini/skills/react/SKILL.md +0 -332
- package/.agents/generated/gemini/skills/react-best-practices/SKILL.md +0 -152
- package/.agents/generated/gemini/skills/redesign-audit/SKILL.md +0 -118
- package/.agents/generated/gemini/skills/soft-design/SKILL.md +0 -109
- package/.agents/generated/gemini/skills/state-management/SKILL.md +0 -129
- package/.agents/generated/gemini/skills/subagent-orchestrator/SKILL.md +0 -116
- package/.agents/generated/gemini/skills/system-design/SKILL.md +0 -631
- package/.agents/generated/gemini/skills/testing/SKILL.md +0 -166
- package/.agents/generated/gemini/skills/typescript/SKILL.md +0 -275
- package/.agents/generated/gemini/skills/ui-design/SKILL.md +0 -170
- package/.agents/generated/gemini/skills/ui-ux-pro/SKILL.md +0 -460
- package/.agents/generated/gemini/skills/ux-design/SKILL.md +0 -177
- package/.agents/generated/gemini/skills/vercel-optimize/SKILL.md +0 -82
- package/.agents/generated/gemini/skills/web-accessibility/SKILL.md +0 -300
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/filesystem/lockfile-v2.js
|
|
3
|
+
* ContextOS Lockfile v2 Schema Manager & CAS Revision Tracker
|
|
4
|
+
*
|
|
5
|
+
* Implements:
|
|
6
|
+
* - Dual-hashing: Exact byte SHA-256 + CRLF-normalized semantic text SHA-256
|
|
7
|
+
* - Monotonic revision tracking with Compare-And-Swap (CAS) concurrency guard
|
|
8
|
+
* - Schema v2 validation adhering to .agents/schemas/lockfile.v2.schema.json
|
|
9
|
+
* - Generator provenance and artifact metadata management
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const { safeRenameSync } = require('./safe-path.js');
|
|
16
|
+
|
|
17
|
+
const DEFAULT_LOCKFILE_SUBPATH = path.join('.agents', 'lockfile.v2.json');
|
|
18
|
+
const SCHEMA_PATH = path.join(__dirname, '..', 'schemas', 'lockfile.v2.schema.json');
|
|
19
|
+
|
|
20
|
+
const ERROR_CODES = {
|
|
21
|
+
INVALID_SCHEMA: 'CTX_LOCKFILE_INVALID_SCHEMA',
|
|
22
|
+
REVISION_CONFLICT: 'CTX_LOCKFILE_REVISION_CONFLICT',
|
|
23
|
+
CORRUPT: 'CTX_LOCKFILE_CORRUPT',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
class LockfileV2Error extends Error {
|
|
27
|
+
constructor(code, message, details = {}) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'LockfileV2Error';
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.details = details;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Computes exact SHA-256 hash of raw byte buffer or string.
|
|
37
|
+
* Format: sha256:<hex>
|
|
38
|
+
*/
|
|
39
|
+
function computeExactHash(content) {
|
|
40
|
+
const buf = Buffer.isBuffer(content) ? content : Buffer.from(String(content), 'utf8');
|
|
41
|
+
const hash = crypto.createHash('sha256').update(buf).digest('hex');
|
|
42
|
+
return `sha256:${hash}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Computes semantic CRLF-normalized text SHA-256 hash to prevent false dirty states across Git checkouts.
|
|
47
|
+
* Format: sha256:<hex>
|
|
48
|
+
*/
|
|
49
|
+
function computeSemanticHash(content) {
|
|
50
|
+
const str = Buffer.isBuffer(content) ? content.toString('utf8') : String(content);
|
|
51
|
+
// Normalize Windows CRLF to LF and trim trailing carriage returns
|
|
52
|
+
const normalized = str.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
53
|
+
const hash = crypto.createHash('sha256').update(normalized, 'utf8').digest('hex');
|
|
54
|
+
return `sha256:${hash}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class LockfileV2Manager {
|
|
58
|
+
constructor(projectRoot, options = {}) {
|
|
59
|
+
this.projectRoot = path.resolve(projectRoot);
|
|
60
|
+
this.lockfilePath = options.lockfilePath
|
|
61
|
+
? path.resolve(this.projectRoot, options.lockfilePath)
|
|
62
|
+
: path.resolve(this.projectRoot, DEFAULT_LOCKFILE_SUBPATH);
|
|
63
|
+
this.schema = this.loadSchema();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
loadSchema() {
|
|
67
|
+
try {
|
|
68
|
+
if (fs.existsSync(SCHEMA_PATH)) {
|
|
69
|
+
return JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Best-effort schema load
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Initializes a fresh Lockfile v2 data structure.
|
|
79
|
+
*/
|
|
80
|
+
createEmpty(meta = {}) {
|
|
81
|
+
return {
|
|
82
|
+
schemaVersion: 2,
|
|
83
|
+
revision: 1,
|
|
84
|
+
package: {
|
|
85
|
+
name: meta.packageName || 'contextos-project',
|
|
86
|
+
version: meta.packageVersion || '1.0.0',
|
|
87
|
+
compilerVersion: meta.compilerVersion || '2.0.0',
|
|
88
|
+
},
|
|
89
|
+
profile: {
|
|
90
|
+
id: meta.profileId || 'default',
|
|
91
|
+
hash: meta.profileHash || 'sha256:0000000000000000000000000000000000000000000000000000000000000000',
|
|
92
|
+
},
|
|
93
|
+
sourceGraphHash: meta.sourceGraphHash || 'sha256:0000000000000000000000000000000000000000000000000000000000000000',
|
|
94
|
+
enabledAdapters: meta.enabledAdapters || [],
|
|
95
|
+
managedFiles: {},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reads and parses Lockfile v2 from disk. Returns null if file does not exist.
|
|
101
|
+
*/
|
|
102
|
+
read() {
|
|
103
|
+
if (!fs.existsSync(this.lockfilePath)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const raw = fs.readFileSync(this.lockfilePath, 'utf8');
|
|
109
|
+
const parsed = JSON.parse(raw);
|
|
110
|
+
this.validate(parsed);
|
|
111
|
+
return parsed;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (err instanceof LockfileV2Error) throw err;
|
|
114
|
+
throw new LockfileV2Error(
|
|
115
|
+
ERROR_CODES.CORRUPT,
|
|
116
|
+
`Lockfile at '${this.lockfilePath}' is corrupt or unparseable: ${err.message}`,
|
|
117
|
+
{ error: err.message, lockfilePath: this.lockfilePath }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validates lockfile structure against LockfileV2 specification.
|
|
124
|
+
*/
|
|
125
|
+
validate(data) {
|
|
126
|
+
if (!data || typeof data !== 'object') {
|
|
127
|
+
throw new LockfileV2Error(ERROR_CODES.INVALID_SCHEMA, 'Lockfile data must be a non-null object');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (data.schemaVersion !== 2) {
|
|
131
|
+
throw new LockfileV2Error(
|
|
132
|
+
ERROR_CODES.INVALID_SCHEMA,
|
|
133
|
+
`Expected schemaVersion 2, got '${data.schemaVersion}'`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof data.revision !== 'number' || data.revision < 1) {
|
|
138
|
+
throw new LockfileV2Error(
|
|
139
|
+
ERROR_CODES.INVALID_SCHEMA,
|
|
140
|
+
`Lockfile revision must be a positive integer, got '${data.revision}'`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!data.package || !data.package.name || !data.package.version) {
|
|
145
|
+
throw new LockfileV2Error(
|
|
146
|
+
ERROR_CODES.INVALID_SCHEMA,
|
|
147
|
+
'Lockfile missing required package identity (name, version)'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!data.profile || !data.profile.id || !data.profile.hash) {
|
|
152
|
+
throw new LockfileV2Error(
|
|
153
|
+
ERROR_CODES.INVALID_SCHEMA,
|
|
154
|
+
'Lockfile missing required profile metadata (id, hash)'
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!Array.isArray(data.enabledAdapters)) {
|
|
159
|
+
throw new LockfileV2Error(
|
|
160
|
+
ERROR_CODES.INVALID_SCHEMA,
|
|
161
|
+
'Lockfile enabledAdapters must be an array'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!data.managedFiles || typeof data.managedFiles !== 'object') {
|
|
166
|
+
throw new LockfileV2Error(
|
|
167
|
+
ERROR_CODES.INVALID_SCHEMA,
|
|
168
|
+
'Lockfile managedFiles must be an object map'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Validate individual file records
|
|
173
|
+
for (const [filePath, record] of Object.entries(data.managedFiles)) {
|
|
174
|
+
if (!record.exactSha256 || !record.semanticTextSha256 || !record.kind || !record.generator) {
|
|
175
|
+
throw new LockfileV2Error(
|
|
176
|
+
ERROR_CODES.INVALID_SCHEMA,
|
|
177
|
+
`Managed file '${filePath}' record is missing required fields (exactSha256, semanticTextSha256, kind, generator)`,
|
|
178
|
+
{ filePath, record }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Saves lockfile to disk with optimistic concurrency Compare-And-Swap (CAS).
|
|
188
|
+
*
|
|
189
|
+
* @param {object} data - Lockfile data to write
|
|
190
|
+
* @param {number} [expectedRevision] - Optional expected revision on disk before increment
|
|
191
|
+
* @returns {object} Updated lockfile data with incremented revision
|
|
192
|
+
*/
|
|
193
|
+
write(data, expectedRevision = undefined) {
|
|
194
|
+
const currentOnDisk = this.read();
|
|
195
|
+
|
|
196
|
+
if (expectedRevision !== undefined && currentOnDisk !== null) {
|
|
197
|
+
if (currentOnDisk.revision !== expectedRevision) {
|
|
198
|
+
throw new LockfileV2Error(
|
|
199
|
+
ERROR_CODES.REVISION_CONFLICT,
|
|
200
|
+
`Compare-And-Swap failed: disk revision is ${currentOnDisk.revision}, expected ${expectedRevision}`,
|
|
201
|
+
{ actualRevision: currentOnDisk.revision, expectedRevision }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Increment revision
|
|
207
|
+
const nextRevision = currentOnDisk ? currentOnDisk.revision + 1 : (data.revision || 1);
|
|
208
|
+
const dataToWrite = {
|
|
209
|
+
...data,
|
|
210
|
+
schemaVersion: 2,
|
|
211
|
+
revision: nextRevision,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
this.validate(dataToWrite);
|
|
215
|
+
|
|
216
|
+
const dir = path.dirname(this.lockfilePath);
|
|
217
|
+
if (!fs.existsSync(dir)) {
|
|
218
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const tmpPath = `${this.lockfilePath}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
222
|
+
fs.writeFileSync(tmpPath, JSON.stringify(dataToWrite, null, 2) + '\n', 'utf8');
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
safeRenameSync(tmpPath, this.lockfilePath);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
try {
|
|
228
|
+
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
229
|
+
} catch {
|
|
230
|
+
// Best-effort cleanup
|
|
231
|
+
}
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return dataToWrite;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Adds or updates a managed file entry in memory.
|
|
240
|
+
*/
|
|
241
|
+
recordManagedFile(data, relativePosixPath, fileMeta) {
|
|
242
|
+
if (!data.managedFiles) data.managedFiles = {};
|
|
243
|
+
|
|
244
|
+
data.managedFiles[relativePosixPath] = {
|
|
245
|
+
exactSha256: fileMeta.exactSha256,
|
|
246
|
+
semanticTextSha256: fileMeta.semanticTextSha256,
|
|
247
|
+
kind: fileMeta.kind || 'generated-adapter',
|
|
248
|
+
generator: fileMeta.generator,
|
|
249
|
+
inputsHash: fileMeta.inputsHash || 'sha256:0000000000000000000000000000000000000000000000000000000000000000',
|
|
250
|
+
mode: fileMeta.mode || 420,
|
|
251
|
+
lastTransaction: fileMeta.lastTransaction || undefined,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return data;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Removes a managed file entry from memory.
|
|
259
|
+
*/
|
|
260
|
+
removeManagedFile(data, relativePosixPath) {
|
|
261
|
+
if (data.managedFiles && data.managedFiles[relativePosixPath]) {
|
|
262
|
+
delete data.managedFiles[relativePosixPath];
|
|
263
|
+
}
|
|
264
|
+
return data;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
LockfileV2Manager,
|
|
270
|
+
computeExactHash,
|
|
271
|
+
computeSemanticHash,
|
|
272
|
+
LockfileV2Error,
|
|
273
|
+
ERROR_CODES,
|
|
274
|
+
DEFAULT_LOCKFILE_SUBPATH,
|
|
275
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/filesystem/platform-hardening.js
|
|
3
|
+
* ContextOS — Cross-Platform Hardening & Resilience Engine
|
|
4
|
+
*
|
|
5
|
+
* Implements Section 21 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
|
|
6
|
+
* - 21.1: Platform support contract (Node 18 Core, Node 20 Runtime, filesystem checks)
|
|
7
|
+
* - 21.2: Windows resilience (EPERM/EBUSY rename/unlink retry wrappers, ADS, reserved names)
|
|
8
|
+
* - 21.4: macOS APFS Unicode normalization (NFC standard)
|
|
9
|
+
* - 21.5: UNC / Network filesystem rejection (distributed lock safety)
|
|
10
|
+
* - 21.6: Platform-aware repository fingerprint (no blanket lowercase, git common dir, persisted UUID)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
|
|
20
|
+
function sha256(str) {
|
|
21
|
+
return crypto.createHash('sha256').update(str || '').digest('hex');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks if a given path is an unsupported UNC or Network Share (Section 21.5).
|
|
26
|
+
*
|
|
27
|
+
* @param {string} targetPath
|
|
28
|
+
* @returns {boolean} True if path is UNC or network share
|
|
29
|
+
*/
|
|
30
|
+
function isNetworkOrUNCPath(targetPath) {
|
|
31
|
+
if (typeof targetPath !== 'string') return false;
|
|
32
|
+
const normalized = targetPath.replace(/\\/g, '/');
|
|
33
|
+
|
|
34
|
+
// UNC paths on Windows: //server/share or \\server\share
|
|
35
|
+
if (normalized.startsWith('//')) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Windows extended-length UNC: \\?\UNC\server\share
|
|
40
|
+
if (/^\\\\[?.]\\UNC\\/i.test(targetPath)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validates platform runtime compatibility (Section 21.1).
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} [options]
|
|
51
|
+
* @param {'core'|'runtime'} [options.component='core']
|
|
52
|
+
* @returns {{ supported: boolean, nodeVersion: number, required: number, issues: string[] }}
|
|
53
|
+
*/
|
|
54
|
+
function validatePlatformSupport(options = {}) {
|
|
55
|
+
const component = options.component || 'core';
|
|
56
|
+
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
|
|
57
|
+
const requiredMajor = component === 'runtime' ? 20 : 18;
|
|
58
|
+
const issues = [];
|
|
59
|
+
|
|
60
|
+
if (nodeMajor < requiredMajor) {
|
|
61
|
+
issues.push(
|
|
62
|
+
`Node.js version ${process.versions.node} is below required minimum v${requiredMajor}.x for ContextOS ${component}.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
supported: issues.length === 0,
|
|
68
|
+
nodeVersion: nodeMajor,
|
|
69
|
+
required: requiredMajor,
|
|
70
|
+
issues,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Computes platform-aware repository fingerprint (Section 21.6).
|
|
76
|
+
* Preserves Linux case while normalizing Windows/macOS drive letters and APFS.
|
|
77
|
+
* Incorporates persisted repository UUID and Git common directory identity.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} repoRoot
|
|
80
|
+
* @returns {string} SHA-256 fingerprint
|
|
81
|
+
*/
|
|
82
|
+
function calculateRepositoryFingerprint(repoRoot) {
|
|
83
|
+
const resolved = path.resolve(repoRoot);
|
|
84
|
+
let canonicalPath = resolved;
|
|
85
|
+
|
|
86
|
+
if (process.platform === 'win32') {
|
|
87
|
+
// Normalize drive letter uppercase (e.g. C:) and forward slashes
|
|
88
|
+
canonicalPath = canonicalPath.replace(/^([a-zA-Z]):/, (_, drive) => drive.toUpperCase() + ':');
|
|
89
|
+
canonicalPath = canonicalPath.toLowerCase(); // Case-insensitive on Windows
|
|
90
|
+
} else if (process.platform === 'darwin') {
|
|
91
|
+
// APFS is typically case-insensitive, normalize Unicode NFC
|
|
92
|
+
canonicalPath = canonicalPath.normalize('NFC').toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Persisted repository UUID
|
|
96
|
+
const stateDir = path.join(resolved, '.agents', '.contextos');
|
|
97
|
+
const uuidFile = path.join(stateDir, 'repo-id');
|
|
98
|
+
let repoUuid = '';
|
|
99
|
+
|
|
100
|
+
if (fs.existsSync(uuidFile)) {
|
|
101
|
+
try {
|
|
102
|
+
repoUuid = fs.readFileSync(uuidFile, 'utf8').trim();
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!repoUuid) {
|
|
107
|
+
repoUuid = crypto.randomUUID();
|
|
108
|
+
try {
|
|
109
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
110
|
+
fs.writeFileSync(uuidFile, repoUuid, 'utf8');
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check Git directory identity if present
|
|
115
|
+
let gitIdentity = 'nogit';
|
|
116
|
+
const gitDir = path.join(resolved, '.git');
|
|
117
|
+
if (fs.existsSync(gitDir)) {
|
|
118
|
+
try {
|
|
119
|
+
const gitStat = fs.statSync(gitDir);
|
|
120
|
+
gitIdentity = `${gitStat.ino || 0}:${gitStat.dev || 0}`;
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const payload = `${canonicalPath}|${repoUuid}|${gitIdentity}`;
|
|
125
|
+
return sha256(payload);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Normalizes Unicode text to NFC standard (Section 21.4).
|
|
130
|
+
*
|
|
131
|
+
* @param {string} text
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
function normalizeUnicodeNFC(text) {
|
|
135
|
+
if (typeof text !== 'string') return text;
|
|
136
|
+
return text.normalize('NFC');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Synchronously renames a file with exponential backoff retry for Windows antivirus/indexer contention (Section 21.2).
|
|
141
|
+
*
|
|
142
|
+
* @param {string} source
|
|
143
|
+
* @param {string} destination
|
|
144
|
+
* @param {Object} [options]
|
|
145
|
+
* @param {number} [options.maxRetries=5]
|
|
146
|
+
* @param {number} [options.retryDelayMs=20]
|
|
147
|
+
*/
|
|
148
|
+
function safeRenameSync(source, destination, options = {}) {
|
|
149
|
+
const maxRetries = options.maxRetries || 5;
|
|
150
|
+
const retryDelayMs = options.retryDelayMs || 20;
|
|
151
|
+
|
|
152
|
+
let lastErr = null;
|
|
153
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
154
|
+
try {
|
|
155
|
+
if (process.platform === 'win32' && fs.existsSync(destination)) {
|
|
156
|
+
try { fs.unlinkSync(destination); } catch {}
|
|
157
|
+
}
|
|
158
|
+
fs.renameSync(source, destination);
|
|
159
|
+
return true;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
lastErr = err;
|
|
162
|
+
if (err.code === 'EPERM' || err.code === 'EBUSY' || err.code === 'EACCES') {
|
|
163
|
+
if (attempt < maxRetries) {
|
|
164
|
+
const sleepMs = Math.floor(retryDelayMs * Math.pow(1.5, attempt));
|
|
165
|
+
// Busy-wait briefly for sync operation
|
|
166
|
+
const start = Date.now();
|
|
167
|
+
while (Date.now() - start < sleepMs) {}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw lastErr;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Synchronously unlinks a file with retry for transient Windows locks (Section 21.2).
|
|
180
|
+
*
|
|
181
|
+
* @param {string} targetPath
|
|
182
|
+
* @param {Object} [options]
|
|
183
|
+
* @param {number} [options.maxRetries=5]
|
|
184
|
+
* @param {number} [options.retryDelayMs=20]
|
|
185
|
+
*/
|
|
186
|
+
function safeUnlinkSync(targetPath, options = {}) {
|
|
187
|
+
if (!fs.existsSync(targetPath)) return true;
|
|
188
|
+
|
|
189
|
+
const maxRetries = options.maxRetries || 5;
|
|
190
|
+
const retryDelayMs = options.retryDelayMs || 20;
|
|
191
|
+
|
|
192
|
+
let lastErr = null;
|
|
193
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
194
|
+
try {
|
|
195
|
+
fs.unlinkSync(targetPath);
|
|
196
|
+
return true;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
lastErr = err;
|
|
199
|
+
if (err.code === 'EPERM' || err.code === 'EBUSY' || err.code === 'EACCES') {
|
|
200
|
+
if (attempt < maxRetries) {
|
|
201
|
+
const sleepMs = Math.floor(retryDelayMs * Math.pow(1.5, attempt));
|
|
202
|
+
const start = Date.now();
|
|
203
|
+
while (Date.now() - start < sleepMs) {}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!fs.existsSync(targetPath)) return true;
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
throw lastErr;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
isNetworkOrUNCPath,
|
|
217
|
+
validatePlatformSupport,
|
|
218
|
+
calculateRepositoryFingerprint,
|
|
219
|
+
normalizeUnicodeNFC,
|
|
220
|
+
safeRenameSync,
|
|
221
|
+
safeUnlinkSync,
|
|
222
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/filesystem/project-lock.js
|
|
3
|
+
* ContextOS Project-Wide Inter-Process Mutation Lock
|
|
4
|
+
*
|
|
5
|
+
* Enforces single-writer mutual exclusion during compilation, export, and migration.
|
|
6
|
+
* - Atomic acquisition via exclusive filesystem creation (O_CREAT | O_EXCL / 'wx')
|
|
7
|
+
* - Cryptographic UUID token prevents zombie/stale processes from releasing new locks
|
|
8
|
+
* - Stale process detection via PID liveness probe (kill(pid, 0))
|
|
9
|
+
* - Structured machine-readable diagnostic output (CTX_PROJECT_BUSY)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
const { isNetworkOrUNCPath } = require('./platform-hardening.js');
|
|
17
|
+
|
|
18
|
+
const LOCK_SUBPATH = path.join('.agents', '.contextos', 'locks', 'mutation.lock');
|
|
19
|
+
const ERROR_CODES = {
|
|
20
|
+
BUSY: 'CTX_PROJECT_BUSY',
|
|
21
|
+
INVALID_TOKEN: 'CTX_LOCK_INVALID_TOKEN',
|
|
22
|
+
NOT_LOCKED: 'CTX_LOCK_NOT_LOCKED',
|
|
23
|
+
UNSUPPORTED: 'UNSUPPORTED',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
class ProjectLockError extends Error {
|
|
27
|
+
constructor(code, message, details = {}) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'ProjectLockError';
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.details = details;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a PID is alive on the local system.
|
|
37
|
+
*/
|
|
38
|
+
function isPidAlive(pid) {
|
|
39
|
+
if (typeof pid !== 'number' || pid <= 0) return false;
|
|
40
|
+
try {
|
|
41
|
+
process.kill(pid, 0);
|
|
42
|
+
return true;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return err.code === 'EPERM'; // Process exists but lacks permission to signal
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class ProjectMutationLock {
|
|
49
|
+
constructor(projectRoot, options = {}) {
|
|
50
|
+
if (isNetworkOrUNCPath(projectRoot)) {
|
|
51
|
+
throw new ProjectLockError(
|
|
52
|
+
ERROR_CODES.UNSUPPORTED,
|
|
53
|
+
`UNC/network filesystem mutation is unsupported for projectRoot: ${projectRoot}`,
|
|
54
|
+
{ projectRoot, status: 'UNSUPPORTED' }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
this.projectRoot = path.resolve(projectRoot);
|
|
58
|
+
this.lockPath = options.lockPath
|
|
59
|
+
? path.resolve(this.projectRoot, options.lockPath)
|
|
60
|
+
: path.resolve(this.projectRoot, LOCK_SUBPATH);
|
|
61
|
+
this.lockDir = path.dirname(this.lockPath);
|
|
62
|
+
this.currentToken = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Reads and parses current lock payload, or null if lock doesn't exist.
|
|
67
|
+
*/
|
|
68
|
+
inspect() {
|
|
69
|
+
try {
|
|
70
|
+
if (!fs.existsSync(this.lockPath)) return null;
|
|
71
|
+
const raw = fs.readFileSync(this.lockPath, 'utf8');
|
|
72
|
+
return JSON.parse(raw);
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Checks if lock is currently held.
|
|
80
|
+
*/
|
|
81
|
+
isLocked() {
|
|
82
|
+
return this.inspect() !== null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Attempts to acquire the mutation lock exclusively.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} [meta] - Context metadata
|
|
89
|
+
* @param {string} [meta.command='mutation'] - Triggering command
|
|
90
|
+
* @param {string} [meta.projectFingerprint=''] - Project identity fingerprint
|
|
91
|
+
* @param {boolean} [meta.autoReapStale=true] - Automatically reclaim lock if holder PID is dead
|
|
92
|
+
* @returns {string} The secret lock token required for release
|
|
93
|
+
*/
|
|
94
|
+
acquire(meta = {}) {
|
|
95
|
+
if (!fs.existsSync(this.lockDir)) {
|
|
96
|
+
fs.mkdirSync(this.lockDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const {
|
|
100
|
+
command = 'mutation',
|
|
101
|
+
projectFingerprint = '',
|
|
102
|
+
autoReapStale = true,
|
|
103
|
+
} = meta;
|
|
104
|
+
|
|
105
|
+
// Check for existing lock and inspect holder
|
|
106
|
+
const existing = this.inspect();
|
|
107
|
+
if (existing) {
|
|
108
|
+
if (autoReapStale && existing.pid && !isPidAlive(existing.pid)) {
|
|
109
|
+
// Holder PID is dead, safe to reap stale lock
|
|
110
|
+
this.forceUnlock(`Auto-reaped stale lock from dead PID ${existing.pid}`);
|
|
111
|
+
} else {
|
|
112
|
+
throw new ProjectLockError(
|
|
113
|
+
ERROR_CODES.BUSY,
|
|
114
|
+
`Project is busy: lock is held by PID ${existing.pid} (${existing.command || 'unknown'}) since ${existing.acquiredAt}`,
|
|
115
|
+
{ lockDetails: existing }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const token = crypto.randomUUID();
|
|
121
|
+
const payload = {
|
|
122
|
+
token,
|
|
123
|
+
pid: process.pid,
|
|
124
|
+
processStartedAt: new Date(Date.now() - Math.floor(process.uptime() * 1000)).toISOString(),
|
|
125
|
+
hostname: os.hostname(),
|
|
126
|
+
command,
|
|
127
|
+
projectFingerprint,
|
|
128
|
+
acquiredAt: new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
let fd = null;
|
|
132
|
+
try {
|
|
133
|
+
// 'wx' flag opens for writing, failing if path already exists (O_CREAT | O_EXCL)
|
|
134
|
+
fd = fs.openSync(this.lockPath, 'wx');
|
|
135
|
+
fs.writeFileSync(fd, JSON.stringify(payload, null, 2), 'utf8');
|
|
136
|
+
fs.fsyncSync(fd);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (err.code === 'EEXIST') {
|
|
139
|
+
const currentHolder = this.inspect();
|
|
140
|
+
throw new ProjectLockError(
|
|
141
|
+
ERROR_CODES.BUSY,
|
|
142
|
+
`Project is busy: lock acquired concurrently by another process`,
|
|
143
|
+
{ lockDetails: currentHolder }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
throw err;
|
|
147
|
+
} finally {
|
|
148
|
+
if (fd !== null) {
|
|
149
|
+
try {
|
|
150
|
+
fs.closeSync(fd);
|
|
151
|
+
} catch {
|
|
152
|
+
// Best effort close
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.currentToken = token;
|
|
158
|
+
return token;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Releases lock verifying ownership token.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} [token] - Release token (defaults to currently held token)
|
|
165
|
+
*/
|
|
166
|
+
release(token = this.currentToken) {
|
|
167
|
+
if (!token) {
|
|
168
|
+
throw new ProjectLockError(
|
|
169
|
+
ERROR_CODES.INVALID_TOKEN,
|
|
170
|
+
'Cannot release lock: no token provided and no token actively held in session'
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const current = this.inspect();
|
|
175
|
+
if (!current) {
|
|
176
|
+
this.currentToken = null;
|
|
177
|
+
return; // Already unlocked
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (current.token !== token) {
|
|
181
|
+
throw new ProjectLockError(
|
|
182
|
+
ERROR_CODES.INVALID_TOKEN,
|
|
183
|
+
`Token mismatch: current lock belongs to PID ${current.pid} acquired at ${current.acquiredAt}`,
|
|
184
|
+
{ expectedToken: current.token, attemptedToken: token }
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
fs.unlinkSync(this.lockPath);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (err.code !== 'ENOENT') throw err;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.currentToken = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Administratively forces removal of the lock.
|
|
199
|
+
*/
|
|
200
|
+
forceUnlock(reason = 'Administrative force unlock') {
|
|
201
|
+
try {
|
|
202
|
+
if (fs.existsSync(this.lockPath)) {
|
|
203
|
+
fs.unlinkSync(this.lockPath);
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
if (err.code !== 'ENOENT') throw err;
|
|
207
|
+
}
|
|
208
|
+
this.currentToken = null;
|
|
209
|
+
return { success: true, reason };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
ProjectMutationLock,
|
|
215
|
+
ProjectLockError,
|
|
216
|
+
ERROR_CODES,
|
|
217
|
+
isPidAlive,
|
|
218
|
+
};
|