@geraldmaron/construct 1.4.0 → 1.4.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/bin/construct +2 -1
- package/bin/construct-postinstall.mjs +27 -2
- package/config/tag-vocabulary.json +264 -0
- package/lib/config/schema.mjs +1 -1
- package/lib/doctor/diagnosis.mjs +20 -0
- package/lib/doctor/index.mjs +5 -0
- package/lib/embed/worker.mjs +5 -0
- package/lib/env-config.mjs +10 -2
- package/lib/export-validate.mjs +34 -2
- package/lib/hooks/guard-bash.mjs +3 -1
- package/lib/host/readiness.mjs +109 -0
- package/lib/host-disposition.mjs +10 -1
- package/lib/install/stage-project.mjs +41 -4
- package/lib/intake/prepare.mjs +2 -0
- package/lib/mcp/destructive-approval.mjs +57 -0
- package/lib/mcp/server.mjs +209 -35
- package/lib/mcp/tool-rate-limit.mjs +47 -0
- package/lib/mcp/tool-safety.mjs +94 -0
- package/lib/mcp/tool-surface-parity.mjs +60 -0
- package/lib/mcp/tools/orchestration-run.mjs +45 -7
- package/lib/mcp/tools/project.mjs +25 -8
- package/lib/mcp/tools/storage.mjs +9 -1
- package/lib/mcp/tools/web-search-governance.mjs +96 -0
- package/lib/mcp/tools/web-search.mjs +6 -44
- package/lib/mcp-platform-config.mjs +27 -18
- package/lib/oracle/daemon-entry.mjs +6 -0
- package/lib/orchestration/runtime.mjs +81 -42
- package/lib/orchestration/web-capability.mjs +59 -0
- package/lib/orchestration/worker.mjs +263 -19
- package/lib/orchestration-policy.mjs +36 -0
- package/lib/output-quality.mjs +61 -2
- package/lib/path-policy.mjs +56 -0
- package/lib/providers/secret-audit-wiring.mjs +35 -18
- package/lib/providers/secret-resolver.mjs +28 -13
- package/lib/registry/catalog.mjs +6 -0
- package/lib/registry/loader.mjs +7 -1
- package/lib/registry/validate.mjs +3 -3
- package/lib/sandbox.mjs +1 -1
- package/lib/service-manager.mjs +59 -9
- package/lib/storage/admin.mjs +7 -2
- package/package.json +6 -1
- package/registry/agent-manifest.json +117 -0
- package/registry/capabilities.json +1880 -0
- package/schemas/brand-voice.schema.json +24 -0
- package/schemas/capability-registry.schema.json +72 -0
- package/schemas/certification-run.schema.json +130 -0
- package/schemas/demo-recording.schema.json +46 -0
- package/schemas/eval-dataset.schema.json +79 -0
- package/schemas/execution-capability-profile.schema.json +46 -0
- package/schemas/execution-policy.schema.json +114 -0
- package/schemas/improvement-proposal.schema.json +65 -0
- package/schemas/mcp-tool-output.schema.json +61 -0
- package/schemas/platform-capabilities.schema.json +83 -0
- package/schemas/project-config.schema.json +227 -0
- package/schemas/project-demo.schema.json +60 -0
- package/schemas/provider-behavior-matrix.schema.json +91 -0
- package/schemas/scope.schema.json +197 -0
- package/schemas/specialist-trace.schema.json +107 -0
- package/schemas/team.schema.json +99 -0
- package/schemas/unified-registry.schema.json +548 -0
- package/scripts/sync-specialists.mjs +135 -12
- package/specialists/org/specialists/cx-researcher.json +1 -0
- package/specialists/prompts/cx-researcher.md +9 -8
- package/vendor/pandoc-ext/README.md +3 -0
- package/vendor/pandoc-ext/diagram.lua +687 -0
package/bin/construct
CHANGED
|
@@ -514,7 +514,8 @@ async function cmdDoctor() {
|
|
|
514
514
|
const wantsFixMigrateState = wantsFixAll || args.includes('--fix-migrate-state');
|
|
515
515
|
|
|
516
516
|
const checks = [];
|
|
517
|
-
const
|
|
517
|
+
const { DOCTOR_STATES } = await import('../lib/doctor/diagnosis.mjs');
|
|
518
|
+
const add = (label, pass, optional = false, failureState = pass ? DOCTOR_STATES.healthy : null) => checks.push({ label, pass, optional, failureState });
|
|
518
519
|
const { isConstructSourceCheckout } = await import('../lib/doctor/source-checkout.mjs');
|
|
519
520
|
const isSourceCheckout = isConstructSourceCheckout(ROOT_DIR);
|
|
520
521
|
add('specialists/org exists', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'org')));
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { spawnSync } from 'node:child_process';
|
|
22
|
-
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { existsSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
25
|
|
|
@@ -109,13 +109,19 @@ if (consumerPkg.name === '@geraldmaron/construct') {
|
|
|
109
109
|
process.exit(0);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// Track every project mutation for the itemized receipt written at the end.
|
|
113
|
+
// npm has no uninstall lifecycle hook so the manifest is the only machine-readable
|
|
114
|
+
// record of what the hook touched and how to revert it (CX-AUDIT-PACKAGE-004).
|
|
115
|
+
const mutations = [];
|
|
116
|
+
|
|
112
117
|
try {
|
|
113
|
-
stageProjectAdapters({
|
|
118
|
+
const stageResult = stageProjectAdapters({
|
|
114
119
|
projectRoot: initCwd,
|
|
115
120
|
packageRoot: PKG_ROOT,
|
|
116
121
|
pkgVersion: PKG_VERSION,
|
|
117
122
|
log,
|
|
118
123
|
});
|
|
124
|
+
mutations.push({ path: '.construct', type: 'stage', synced: stageResult.synced });
|
|
119
125
|
|
|
120
126
|
// ADR-0027: Ensure .gitignore covers the newly staged adapters (construct-f6l6).
|
|
121
127
|
// Idempotent: missingIgnorePatterns returns only patterns not already present.
|
|
@@ -129,8 +135,27 @@ try {
|
|
|
129
135
|
const block = `${prefix}\n${HEADER}\n${SUBHEADER}\n${missing.join('\n')}\n`;
|
|
130
136
|
const { appendFileSync } = await import('node:fs');
|
|
131
137
|
appendFileSync(giPath, block, 'utf8');
|
|
138
|
+
mutations.push({ path: '.gitignore', type: 'append', patterns: missing });
|
|
132
139
|
log(`appended ${missing.length} Construct ignore pattern(s) to .gitignore`);
|
|
133
140
|
}
|
|
141
|
+
|
|
142
|
+
// Write the itemized mutation receipt so consumers can audit and revert what the
|
|
143
|
+
// hook touched. Write is best-effort: a manifest failure must never break an install.
|
|
144
|
+
try {
|
|
145
|
+
mkdirSync(path.join(initCwd, '.construct'), { recursive: true });
|
|
146
|
+
const manifest = {
|
|
147
|
+
pkgVersion: PKG_VERSION,
|
|
148
|
+
ts: new Date().toISOString(),
|
|
149
|
+
files: mutations.map((m) => m.path),
|
|
150
|
+
mutations,
|
|
151
|
+
recovery: 'npx construct init --repair',
|
|
152
|
+
};
|
|
153
|
+
writeFileSync(
|
|
154
|
+
path.join(initCwd, '.construct', 'install-manifest.json'),
|
|
155
|
+
JSON.stringify(manifest, null, 2) + '\n',
|
|
156
|
+
);
|
|
157
|
+
log('wrote .construct/install-manifest.json');
|
|
158
|
+
} catch { /* manifest write must not fail the install */ }
|
|
134
159
|
} catch (err) {
|
|
135
160
|
fail(`Adapter staging failed: ${err.message}`, 'The package is installed; run `npx construct init` in this project to complete setup.');
|
|
136
161
|
// Intentionally exit 0: staging is best-effort completion, and a non-zero exit
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"facets": {
|
|
4
|
+
"intake-type": { "exclusive": true, "auto_threshold": 0.80 },
|
|
5
|
+
"lifecycle": { "exclusive": true, "auto_threshold": 0.90 },
|
|
6
|
+
"priority": { "exclusive": true, "auto_threshold": 0.90 },
|
|
7
|
+
"signal-strength": { "exclusive": false, "auto_threshold": 0.95 },
|
|
8
|
+
"reversibility": { "exclusive": true, "auto_threshold": 0.85 },
|
|
9
|
+
"doctype": { "exclusive": true, "auto_threshold": 0.90 }
|
|
10
|
+
},
|
|
11
|
+
"tags": [
|
|
12
|
+
{
|
|
13
|
+
"id": "intake/bug",
|
|
14
|
+
"facet": "intake-type",
|
|
15
|
+
"label": "Bug",
|
|
16
|
+
"synonyms": ["defect", "regression", "error"],
|
|
17
|
+
"promote_to_graph": false,
|
|
18
|
+
"status": "active"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "intake/user-signal",
|
|
22
|
+
"facet": "intake-type",
|
|
23
|
+
"label": "User Signal",
|
|
24
|
+
"synonyms": ["feedback", "user-feedback", "customer-signal"],
|
|
25
|
+
"promote_to_graph": false,
|
|
26
|
+
"status": "active"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "intake/experiment",
|
|
30
|
+
"facet": "intake-type",
|
|
31
|
+
"label": "Experiment",
|
|
32
|
+
"synonyms": ["ab-test", "test", "trial"],
|
|
33
|
+
"promote_to_graph": false,
|
|
34
|
+
"status": "active"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "intake/architecture",
|
|
38
|
+
"facet": "intake-type",
|
|
39
|
+
"label": "Architecture",
|
|
40
|
+
"synonyms": ["design", "system-design", "arch"],
|
|
41
|
+
"promote_to_graph": false,
|
|
42
|
+
"status": "active"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "intake/incident",
|
|
46
|
+
"facet": "intake-type",
|
|
47
|
+
"label": "Incident",
|
|
48
|
+
"synonyms": ["outage", "postmortem", "production-issue"],
|
|
49
|
+
"promote_to_graph": false,
|
|
50
|
+
"status": "active"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "intake/security",
|
|
54
|
+
"facet": "intake-type",
|
|
55
|
+
"label": "Security",
|
|
56
|
+
"synonyms": ["vulnerability", "cve", "threat"],
|
|
57
|
+
"promote_to_graph": false,
|
|
58
|
+
"status": "active"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "intake/requirement",
|
|
62
|
+
"facet": "intake-type",
|
|
63
|
+
"label": "Requirement",
|
|
64
|
+
"synonyms": ["spec", "specification", "acceptance-criteria"],
|
|
65
|
+
"promote_to_graph": false,
|
|
66
|
+
"status": "active"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "intake/research",
|
|
70
|
+
"facet": "intake-type",
|
|
71
|
+
"label": "Research",
|
|
72
|
+
"synonyms": ["study", "literature-review"],
|
|
73
|
+
"promote_to_graph": false,
|
|
74
|
+
"status": "active"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "intake/ops",
|
|
78
|
+
"facet": "intake-type",
|
|
79
|
+
"label": "Ops",
|
|
80
|
+
"synonyms": ["operational", "runbook", "infra"],
|
|
81
|
+
"promote_to_graph": false,
|
|
82
|
+
"status": "active"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "intake/eval-finding",
|
|
86
|
+
"facet": "intake-type",
|
|
87
|
+
"label": "Eval Finding",
|
|
88
|
+
"synonyms": ["evaluation", "eval", "benchmark-result"],
|
|
89
|
+
"promote_to_graph": false,
|
|
90
|
+
"status": "active"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "intake/launch-asset",
|
|
94
|
+
"facet": "intake-type",
|
|
95
|
+
"label": "Launch Asset",
|
|
96
|
+
"synonyms": ["launch", "go-to-market", "release-material"],
|
|
97
|
+
"promote_to_graph": false,
|
|
98
|
+
"status": "active"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "intake/legal-compliance",
|
|
102
|
+
"facet": "intake-type",
|
|
103
|
+
"label": "Legal Compliance",
|
|
104
|
+
"synonyms": ["legal", "compliance", "regulatory"],
|
|
105
|
+
"promote_to_graph": false,
|
|
106
|
+
"status": "active"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "lifecycle/draft",
|
|
110
|
+
"facet": "lifecycle",
|
|
111
|
+
"label": "Draft",
|
|
112
|
+
"synonyms": ["wip", "in-progress"],
|
|
113
|
+
"promote_to_graph": false,
|
|
114
|
+
"status": "active"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "lifecycle/approved",
|
|
118
|
+
"facet": "lifecycle",
|
|
119
|
+
"label": "Approved",
|
|
120
|
+
"synonyms": ["accepted", "ratified"],
|
|
121
|
+
"promote_to_graph": false,
|
|
122
|
+
"status": "active"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "lifecycle/deprecated",
|
|
126
|
+
"facet": "lifecycle",
|
|
127
|
+
"label": "Deprecated",
|
|
128
|
+
"synonyms": ["sunset", "retiring"],
|
|
129
|
+
"promote_to_graph": false,
|
|
130
|
+
"status": "active"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "lifecycle/superseded",
|
|
134
|
+
"facet": "lifecycle",
|
|
135
|
+
"label": "Superseded",
|
|
136
|
+
"synonyms": ["replaced", "obsolete"],
|
|
137
|
+
"promote_to_graph": false,
|
|
138
|
+
"status": "active"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "lifecycle/archived",
|
|
142
|
+
"facet": "lifecycle",
|
|
143
|
+
"label": "Archived",
|
|
144
|
+
"synonyms": ["closed", "done"],
|
|
145
|
+
"promote_to_graph": false,
|
|
146
|
+
"status": "active"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "priority/p0",
|
|
150
|
+
"facet": "priority",
|
|
151
|
+
"label": "P0 - Critical",
|
|
152
|
+
"synonyms": ["critical", "blocker", "sev0"],
|
|
153
|
+
"promote_to_graph": true,
|
|
154
|
+
"status": "active"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "priority/p1",
|
|
158
|
+
"facet": "priority",
|
|
159
|
+
"label": "P1 - High",
|
|
160
|
+
"synonyms": ["high", "urgent", "sev1"],
|
|
161
|
+
"promote_to_graph": true,
|
|
162
|
+
"status": "active"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "priority/p2",
|
|
166
|
+
"facet": "priority",
|
|
167
|
+
"label": "P2 - Medium",
|
|
168
|
+
"synonyms": ["medium", "normal", "sev2"],
|
|
169
|
+
"promote_to_graph": false,
|
|
170
|
+
"status": "active"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "priority/p3",
|
|
174
|
+
"facet": "priority",
|
|
175
|
+
"label": "P3 - Low",
|
|
176
|
+
"synonyms": ["low", "nice-to-have", "sev3"],
|
|
177
|
+
"promote_to_graph": false,
|
|
178
|
+
"status": "active"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"id": "signal/high-customer-interest",
|
|
182
|
+
"facet": "signal-strength",
|
|
183
|
+
"label": "High Customer Interest",
|
|
184
|
+
"synonyms": ["customer-demand", "high-demand", "top-request"],
|
|
185
|
+
"promote_to_graph": true,
|
|
186
|
+
"anchor_entities": ["customer-feedback"],
|
|
187
|
+
"expected_cardinality": 100,
|
|
188
|
+
"cardinality_window_days": 90,
|
|
189
|
+
"status": "active"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"id": "signal/internal-alignment",
|
|
193
|
+
"facet": "signal-strength",
|
|
194
|
+
"label": "Internal Alignment",
|
|
195
|
+
"synonyms": ["aligned", "consensus", "cross-team"],
|
|
196
|
+
"promote_to_graph": false,
|
|
197
|
+
"status": "active"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"id": "signal/blocked",
|
|
201
|
+
"facet": "signal-strength",
|
|
202
|
+
"label": "Blocked",
|
|
203
|
+
"synonyms": ["dependency", "waiting", "stalled"],
|
|
204
|
+
"promote_to_graph": false,
|
|
205
|
+
"status": "active"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"id": "reversibility/irreversible",
|
|
209
|
+
"facet": "reversibility",
|
|
210
|
+
"label": "Irreversible",
|
|
211
|
+
"synonyms": ["one-way-door", "permanent"],
|
|
212
|
+
"promote_to_graph": true,
|
|
213
|
+
"status": "active"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"id": "reversibility/reversible",
|
|
217
|
+
"facet": "reversibility",
|
|
218
|
+
"label": "Reversible",
|
|
219
|
+
"synonyms": ["two-way-door", "rollback"],
|
|
220
|
+
"promote_to_graph": false,
|
|
221
|
+
"status": "active"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"id": "reversibility/quasi-reversible",
|
|
225
|
+
"facet": "reversibility",
|
|
226
|
+
"label": "Quasi-Reversible",
|
|
227
|
+
"synonyms": ["partially-reversible", "conditional-rollback"],
|
|
228
|
+
"promote_to_graph": false,
|
|
229
|
+
"status": "active"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"id": "doctype/prd",
|
|
233
|
+
"facet": "doctype",
|
|
234
|
+
"label": "PRD",
|
|
235
|
+
"synonyms": ["product-requirements", "product-spec"],
|
|
236
|
+
"promote_to_graph": false,
|
|
237
|
+
"status": "active"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"id": "doctype/adr",
|
|
241
|
+
"facet": "doctype",
|
|
242
|
+
"label": "ADR",
|
|
243
|
+
"synonyms": ["architecture-decision", "decision-record"],
|
|
244
|
+
"promote_to_graph": false,
|
|
245
|
+
"status": "active"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"id": "doctype/rfc",
|
|
249
|
+
"facet": "doctype",
|
|
250
|
+
"label": "RFC",
|
|
251
|
+
"synonyms": ["request-for-comments", "proposal"],
|
|
252
|
+
"promote_to_graph": false,
|
|
253
|
+
"status": "active"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"id": "doctype/postmortem",
|
|
257
|
+
"facet": "doctype",
|
|
258
|
+
"label": "Postmortem",
|
|
259
|
+
"synonyms": ["incident-review", "retrospective"],
|
|
260
|
+
"promote_to_graph": false,
|
|
261
|
+
"status": "active"
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
}
|
package/lib/config/schema.mjs
CHANGED
|
@@ -156,7 +156,7 @@ export const FIELD_RULES = {
|
|
|
156
156
|
types: { type: 'object', required: false },
|
|
157
157
|
},
|
|
158
158
|
},
|
|
159
|
-
scope: { type: 'string', required: false, maxLength: 40 },
|
|
159
|
+
scope: { type: 'string', required: false, maxLength: 40, enum: ['rnd', 'operations', 'creative', 'research'] },
|
|
160
160
|
autoEmbed: { type: 'boolean', required: false },
|
|
161
161
|
ingest: {
|
|
162
162
|
type: 'object',
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doctor/diagnosis.mjs — typed failure-state taxonomy for the doctor command.
|
|
3
|
+
*
|
|
4
|
+
* Each constant names a distinct class of host / package / MCP / runtime / secrets
|
|
5
|
+
* failure so doctor findings carry an actionable code rather than a bare boolean.
|
|
6
|
+
* Consumers (bin/construct cmdDoctor, lib/host/readiness.mjs, tests) import these
|
|
7
|
+
* keys to ensure the full taxonomy is covered and no failure collapses to a generic
|
|
8
|
+
* "unhealthy" verdict.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const DOCTOR_STATES = {
|
|
12
|
+
'missing-config': 'A required configuration file or directory is absent.',
|
|
13
|
+
'stale-path': 'A registered path (toolkit, entrypoint, server) is outdated or points nowhere.',
|
|
14
|
+
'disabled': 'A feature or gate is explicitly disabled in config.',
|
|
15
|
+
'server-start-failure': 'The MCP or dashboard server failed to start or did not respond within the timeout.',
|
|
16
|
+
'secret-leak': 'A credential or secret is exposed in a tracked file or insecure location.',
|
|
17
|
+
'entrypoint-missing': 'A binary or script the runtime expects (construct-mcp, construct) is not found on PATH.',
|
|
18
|
+
'healthy': 'The check passed with no issues detected.',
|
|
19
|
+
'degraded': 'The check passed with warnings; functionality may be reduced.',
|
|
20
|
+
};
|
package/lib/doctor/index.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { writeFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } from '
|
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
|
|
17
17
|
import { record } from './audit.mjs';
|
|
18
|
+
import { enableSecretAuditTrail } from '../providers/secret-audit-wiring.mjs';
|
|
18
19
|
import * as disk from './watchers/disk.mjs';
|
|
19
20
|
import * as cost from './watchers/cost.mjs';
|
|
20
21
|
import * as processPressure from './watchers/process-pressure.mjs';
|
|
@@ -140,5 +141,9 @@ const isMain = (() => {
|
|
|
140
141
|
try { return import.meta.url === `file://${process.argv[1]}`; } catch { return false; }
|
|
141
142
|
})();
|
|
142
143
|
if (isMain) {
|
|
144
|
+
// A long-lived daemon in its own process: wire the audit sink at the entry so any
|
|
145
|
+
// credential resolution a watcher performs is recorded on the shared trail rather
|
|
146
|
+
// than escaping it, matching the CLI entrypoint's own wiring.
|
|
147
|
+
enableSecretAuditTrail();
|
|
143
148
|
await start();
|
|
144
149
|
}
|
package/lib/embed/worker.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { prepareConstructEnv } from '../runtime-env.mjs';
|
|
12
|
+
import { enableSecretAuditTrail } from '../providers/secret-audit-wiring.mjs';
|
|
12
13
|
import { EmbedDaemon } from './daemon.mjs';
|
|
13
14
|
|
|
14
15
|
function parseArgv(argv) {
|
|
@@ -18,6 +19,10 @@ function parseArgv(argv) {
|
|
|
18
19
|
return null;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
// Snapshot ticks resolve provider credentials in the daemon's own process; wiring
|
|
23
|
+
// the sink here records those op reads on the same trail as the CLI entrypoint.
|
|
24
|
+
enableSecretAuditTrail();
|
|
25
|
+
|
|
21
26
|
const configPath = parseArgv(process.argv.slice(2));
|
|
22
27
|
const env = prepareConstructEnv();
|
|
23
28
|
|
package/lib/env-config.mjs
CHANGED
|
@@ -52,7 +52,15 @@ export function writeEnvValues(filePath, values = {}) {
|
|
|
52
52
|
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
53
53
|
.map(([key, value]) => `${key}=${value}`)
|
|
54
54
|
.join('\n');
|
|
55
|
-
fs.writeFileSync(filePath, `${content}\n`, 'utf8');
|
|
55
|
+
fs.writeFileSync(filePath, `${content}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
56
|
+
|
|
57
|
+
// config.env can hold plaintext credentials. writeFileSync keeps an existing inode's
|
|
58
|
+
// mode, so the mode option does not tighten a file an earlier write (or a caller that
|
|
59
|
+
// skipped its own chmod) left 0644; re-apply 0600 on every write so the credential
|
|
60
|
+
// file is never group/world readable.
|
|
61
|
+
if (process.platform !== 'win32') {
|
|
62
|
+
try { fs.chmodSync(filePath, 0o600); } catch { /* exotic filesystem */ }
|
|
63
|
+
}
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
function cleanEnvValue(value) {
|
|
@@ -103,7 +111,7 @@ function isOpReference(value) {
|
|
|
103
111
|
return Boolean(extractOpRef(value));
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
function preferInjectedCredential(envValue, fileValue) {
|
|
114
|
+
export function preferInjectedCredential(envValue, fileValue) {
|
|
107
115
|
return Boolean(
|
|
108
116
|
envValue
|
|
109
117
|
&& !isOpReference(envValue)
|
package/lib/export-validate.mjs
CHANGED
|
@@ -86,21 +86,53 @@ export function contentRoundtrip({ exportPath, format, sourceMarkdown, env = pro
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// Local image and link targets must resolve on disk; remote URLs, anchors, and mailto are out of
|
|
89
|
-
// scope for an on-disk integrity check.
|
|
89
|
+
// scope for an on-disk integrity check. Three reference forms in source are covered: ![]() syntax,
|
|
90
|
+
// raw inline <img src> HTML, and reference-style definitions ([id]: ./path). The produced HTML
|
|
91
|
+
// file's own <img src> references are also scanned, catching assets referenced only in the export.
|
|
90
92
|
|
|
91
|
-
export function referenceIntegrity(sourceMarkdown, baseDir) {
|
|
93
|
+
export function referenceIntegrity(sourceMarkdown, baseDir, exportPath = null) {
|
|
92
94
|
const body = sourceMarkdown.replace(/```[\s\S]*?```/g, '');
|
|
93
95
|
const missingImages = [];
|
|
94
96
|
const brokenLinks = [];
|
|
97
|
+
|
|
98
|
+
// Standard Markdown image syntax: 
|
|
95
99
|
for (const match of body.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
96
100
|
const ref = match[1].split(/\s+/)[0];
|
|
97
101
|
if (/^https?:|^data:/.test(ref)) continue;
|
|
98
102
|
if (!fs.existsSync(path.resolve(baseDir, ref))) missingImages.push(ref);
|
|
99
103
|
}
|
|
104
|
+
|
|
105
|
+
// Raw inline HTML <img src> in source markdown
|
|
106
|
+
for (const match of body.matchAll(/<img[^>]+src\s*=\s*["']([^"']+)["']/gi)) {
|
|
107
|
+
const ref = match[1];
|
|
108
|
+
if (/^https?:|^data:|^#/.test(ref)) continue;
|
|
109
|
+
if (!fs.existsSync(path.resolve(baseDir, ref))) missingImages.push(ref);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Reference-style image/link definitions: [id]: ./path
|
|
113
|
+
for (const match of body.matchAll(/^\[[^\]]+\]:\s+(\S+)/gm)) {
|
|
114
|
+
const ref = match[1];
|
|
115
|
+
if (/^https?:|^#|^mailto:/.test(ref)) continue;
|
|
116
|
+
if (!fs.existsSync(path.resolve(baseDir, ref))) missingImages.push(ref);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Standard Markdown link syntax: [text](path) (non-image)
|
|
100
120
|
for (const match of body.matchAll(/(?<!!)\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
101
121
|
const ref = match[1].split(/\s+/)[0];
|
|
102
122
|
if (/^https?:|^#|^mailto:/.test(ref)) continue;
|
|
103
123
|
if (!fs.existsSync(path.resolve(baseDir, ref))) brokenLinks.push(ref);
|
|
104
124
|
}
|
|
125
|
+
|
|
126
|
+
// Produced HTML file: scan internal <img src> references that may not appear in source markdown
|
|
127
|
+
if (exportPath && exportPath.endsWith('.html') && fs.existsSync(exportPath)) {
|
|
128
|
+
let html = '';
|
|
129
|
+
try { html = fs.readFileSync(exportPath, 'utf8'); } catch { /* skip */ }
|
|
130
|
+
for (const match of html.matchAll(/<img[^>]+src\s*=\s*["']([^"']+)["']/gi)) {
|
|
131
|
+
const ref = match[1];
|
|
132
|
+
if (/^https?:|^data:|^#/.test(ref)) continue;
|
|
133
|
+
if (!fs.existsSync(path.resolve(baseDir, ref))) missingImages.push(ref);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
105
137
|
return { ok: missingImages.length === 0 && brokenLinks.length === 0, missingImages, brokenLinks };
|
|
106
138
|
}
|
package/lib/hooks/guard-bash.mjs
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Runs as PreToolUse on Bash. Scans the command against a blocklist of destructive patterns (rm -rf, force push to main, etc.) and exits 2 to block matches.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Budget covers the role-fence path below — doctorRoot lookup, last-agent file reads, and the lazy manifest/fence/approval imports — which the original 5ms target predated; the nightly bench (variance-heavy CI lane) measured ~17-19ms marginal, so the budget reflects that real cost with the ×2 gate tolerance still catching a true regression.
|
|
8
|
+
*
|
|
9
|
+
* @p95ms 15
|
|
8
10
|
* @maxBlockingScope PreToolUse
|
|
9
11
|
*
|
|
10
12
|
* @lifecycle PreToolUse
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/host/readiness.mjs — host-config readiness classifier for VS Code / Copilot.
|
|
3
|
+
*
|
|
4
|
+
* Distinguishes the discrete states a host-config can be in — from missing config
|
|
5
|
+
* through various partial-setup failure modes to healthy — rather than collapsing
|
|
6
|
+
* readiness to a binary file-presence check. Each state has an associated next-step
|
|
7
|
+
* hint surfaced by `construct doctor` to guide the developer toward a working setup.
|
|
8
|
+
*
|
|
9
|
+
* classifyHostReadiness({ host, settingsPath, mcpPath, root }) returns a reasonCode
|
|
10
|
+
* from HOST_READINESS_REASONS. Resolution order: missing_config → stale_path →
|
|
11
|
+
* jsonc_unpatched → wrong_key → disabled → healthy (runtime states untrusted /
|
|
12
|
+
* server_start_failure / missing_tool / sandbox_disabled require a live host session
|
|
13
|
+
* and are returned when the caller supplies runtimeState evidence).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
export const HOST_READINESS_REASONS = Object.freeze([
|
|
20
|
+
'missing_config',
|
|
21
|
+
'stale_path',
|
|
22
|
+
'jsonc_unpatched',
|
|
23
|
+
'wrong_key',
|
|
24
|
+
'untrusted',
|
|
25
|
+
'disabled',
|
|
26
|
+
'server_start_failure',
|
|
27
|
+
'missing_tool',
|
|
28
|
+
'sandbox_disabled',
|
|
29
|
+
'healthy',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export const HOST_READINESS_NEXT_STEPS = Object.freeze({
|
|
33
|
+
missing_config: 'Run `construct sync` to generate host config files.',
|
|
34
|
+
stale_path: 'Run `construct sync` to refresh the MCP server path.',
|
|
35
|
+
jsonc_unpatched: 'Run `construct sync` to merge managed settings into your settings.json.',
|
|
36
|
+
wrong_key: 'Run `construct sync` to correct the MCP autostart setting key.',
|
|
37
|
+
untrusted: 'Open VS Code, find the MCP server notification, and grant trust.',
|
|
38
|
+
disabled: 'Set `chat.mcp.autoStart: "always"` in your VS Code settings.',
|
|
39
|
+
server_start_failure: 'Check the MCP server log: `construct mcp:logs`.',
|
|
40
|
+
missing_tool: 'Run `construct sync` and restart VS Code.',
|
|
41
|
+
sandbox_disabled: 'Enable the MCP sandbox in VS Code extension settings.',
|
|
42
|
+
healthy: null,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const MANAGED_MCP_KEY_PATTERN = /\/lib\/mcp\/[a-z0-9-]+\.mjs$/i;
|
|
46
|
+
const AUTOSTART_KEY = 'chat.mcp.autoStart';
|
|
47
|
+
const AUTOSTART_KEY_WRONG_CASE_PATTERN = /^chat\.mcp\.autostart$/i;
|
|
48
|
+
|
|
49
|
+
function tryReadJson(filePath) {
|
|
50
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
51
|
+
catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isJsoncFile(filePath) {
|
|
55
|
+
try {
|
|
56
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
57
|
+
JSON.parse(text);
|
|
58
|
+
return false;
|
|
59
|
+
} catch {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function classifyHostReadiness({
|
|
65
|
+
host = 'vscode',
|
|
66
|
+
settingsPath = null,
|
|
67
|
+
mcpPath = null,
|
|
68
|
+
root = null,
|
|
69
|
+
runtimeState = null,
|
|
70
|
+
} = {}) {
|
|
71
|
+
if (mcpPath && !fs.existsSync(mcpPath)) return 'missing_config';
|
|
72
|
+
|
|
73
|
+
if (mcpPath && root) {
|
|
74
|
+
const mcp = tryReadJson(mcpPath);
|
|
75
|
+
if (mcp) {
|
|
76
|
+
const servers = mcp.servers ?? {};
|
|
77
|
+
const hasStale = Object.values(servers).some((s) => {
|
|
78
|
+
const args = Array.isArray(s?.args) ? s.args : [];
|
|
79
|
+
return args.some((arg) => {
|
|
80
|
+
if (typeof arg !== 'string') return false;
|
|
81
|
+
const normalArg = arg.replace(/\\/g, '/');
|
|
82
|
+
const normalRoot = root.replace(/\\/g, '/');
|
|
83
|
+
return MANAGED_MCP_KEY_PATTERN.test(normalArg) && !normalArg.startsWith(`${normalRoot}/`);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
if (hasStale) return 'stale_path';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (settingsPath) {
|
|
91
|
+
if (fs.existsSync(settingsPath)) {
|
|
92
|
+
if (isJsoncFile(settingsPath)) return 'jsonc_unpatched';
|
|
93
|
+
const settings = tryReadJson(settingsPath);
|
|
94
|
+
if (settings) {
|
|
95
|
+
const hasWrongCase = Object.keys(settings).some(
|
|
96
|
+
(k) => k !== AUTOSTART_KEY && AUTOSTART_KEY_WRONG_CASE_PATTERN.test(k),
|
|
97
|
+
);
|
|
98
|
+
if (hasWrongCase) return 'wrong_key';
|
|
99
|
+
if (settings[AUTOSTART_KEY] === 'never' || settings[AUTOSTART_KEY] === false) {
|
|
100
|
+
return 'disabled';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (runtimeState) return runtimeState;
|
|
107
|
+
|
|
108
|
+
return 'healthy';
|
|
109
|
+
}
|
package/lib/host-disposition.mjs
CHANGED
|
@@ -41,13 +41,22 @@ export const IGNORED_PATTERNS = [
|
|
|
41
41
|
'.construct/',
|
|
42
42
|
'.mcp.json',
|
|
43
43
|
'.github/prompts/',
|
|
44
|
-
'.github/
|
|
44
|
+
'.github/agents/',
|
|
45
45
|
'config.env',
|
|
46
46
|
'embed.yaml',
|
|
47
47
|
'plan.md',
|
|
48
48
|
'inbox/',
|
|
49
49
|
];
|
|
50
50
|
|
|
51
|
+
// User-owned files Construct mutates only via replaceManagedBlock (ADR-0027 §2).
|
|
52
|
+
// These are durable repo source, not ignored cache — they must NOT appear in
|
|
53
|
+
// IGNORED_PATTERNS, but they still need an explicit disposition so consumers
|
|
54
|
+
// know not to gitignore or delete them on uninstall.
|
|
55
|
+
|
|
56
|
+
export const MANAGED_PATTERNS = [
|
|
57
|
+
'.github/copilot-instructions.md',
|
|
58
|
+
];
|
|
59
|
+
|
|
51
60
|
// A pattern counts as already-ignored when an existing line matches it exactly,
|
|
52
61
|
// matches its bare/slashed form, or is a broader `*` / `**` catch-all.
|
|
53
62
|
|