@opengsd/gsd-core 1.6.1 → 1.7.0-rc.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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +711 -0
- package/agents/gsd-advisor-researcher.md +2 -0
- package/agents/gsd-ai-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +2 -0
- package/agents/gsd-code-fixer.md +2 -0
- package/agents/gsd-code-reviewer.md +2 -0
- package/agents/gsd-codebase-mapper.md +2 -0
- package/agents/gsd-debugger.md +2 -0
- package/agents/gsd-doc-writer.md +2 -0
- package/agents/gsd-eval-auditor.md +2 -0
- package/agents/gsd-executor.md +9 -6
- package/agents/gsd-integration-checker.md +2 -0
- package/agents/gsd-nyquist-auditor.md +2 -0
- package/agents/gsd-phase-researcher.md +2 -0
- package/agents/gsd-plan-checker.md +2 -0
- package/agents/gsd-planner.md +2 -0
- package/agents/gsd-project-researcher.md +2 -0
- package/agents/gsd-research-synthesizer.md +2 -0
- package/agents/gsd-roadmapper.md +2 -0
- package/agents/gsd-security-auditor.md +2 -0
- package/agents/gsd-ui-auditor.md +2 -0
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +2 -0
- package/agents/gsd-verifier.md +5 -2
- package/bin/gsd-mcp-server.js +31 -0
- package/bin/install.js +411 -1146
- package/commands/gsd/review.md +6 -0
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +134 -8
- package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
- package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
- package/gsd-core/bin/lib/capability-loader.cjs +45 -9
- package/gsd-core/bin/lib/capability-lock.cjs +2 -2
- package/gsd-core/bin/lib/capability-registry.cjs +891 -82
- package/gsd-core/bin/lib/capability-source.cjs +26 -11
- package/gsd-core/bin/lib/capability-validator.cjs +222 -2
- package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +2 -1
- package/gsd-core/bin/lib/config.cjs +27 -0
- package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
- package/gsd-core/bin/lib/frontmatter.cjs +53 -6
- package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
- package/gsd-core/bin/lib/hook-bus.cjs +81 -0
- package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
- package/gsd-core/bin/lib/host-integration.cjs +469 -0
- package/gsd-core/bin/lib/init.cjs +35 -7
- package/gsd-core/bin/lib/install-engine.cjs +755 -0
- package/gsd-core/bin/lib/install-profiles.cjs +35 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/mcp-server.cjs +194 -0
- package/gsd-core/bin/lib/milestone.cjs +68 -40
- package/gsd-core/bin/lib/model-adapter.cjs +50 -0
- package/gsd-core/bin/lib/phase-id.cjs +18 -0
- package/gsd-core/bin/lib/phase.cjs +57 -90
- package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
- package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
- package/gsd-core/bin/lib/probe-core.cjs +132 -2
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
- package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
- package/gsd-core/bin/lib/roadmap.cjs +33 -22
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +16 -0
- package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
- package/gsd-core/bin/lib/state-command-router.cjs +4 -0
- package/gsd-core/bin/lib/state-io.cjs +55 -0
- package/gsd-core/bin/lib/state-transition.cjs +1603 -0
- package/gsd-core/bin/lib/state.cjs +327 -683
- package/gsd-core/bin/lib/surface.cjs +4 -1
- package/gsd-core/bin/lib/validate.cjs +2 -1
- package/gsd-core/bin/lib/verify.cjs +6 -4
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
- package/gsd-core/bin/lib/workstream.cjs +4 -4
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
- package/gsd-core/references/agent-skills-bootstrap.md +60 -0
- package/gsd-core/references/honest-verifier.md +105 -0
- package/gsd-core/references/model-profiles.md +27 -0
- package/gsd-core/references/reviewer-instances.md +99 -0
- package/gsd-core/workflows/autonomous.md +30 -32
- package/gsd-core/workflows/complete-milestone.md +6 -10
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/forensics.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/manager.md +15 -15
- package/gsd-core/workflows/milestone-summary.md +3 -3
- package/gsd-core/workflows/new-milestone.md +6 -0
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
- package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
- package/gsd-core/workflows/plan-phase.md +4 -159
- package/gsd-core/workflows/review.md +33 -2
- package/gsd-core/workflows/thread.md +4 -4
- package/gsd-core/workflows/verify-phase.md +11 -4
- package/gsd-core/workflows/verify-work.md +1 -2
- package/hooks/dist/gsd-graphify-update.sh +7 -1
- package/hooks/gsd-graphify-update.sh +7 -1
- package/package.json +6 -4
- package/scripts/ci-test-scope.cjs +38 -9
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-regression-test-names.allowlist.json +3 -0
- package/scripts/lint-test-file-count.allowlist.json +19 -5
- package/scripts/mutation-matrix.cjs +45 -3
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/run-tests.cjs +51 -1
- package/scripts/sync-manifest-versions.cjs +66 -14
- package/skills/gsd-review/SKILL.md +6 -0
- package/scripts/lint-windows-test-portability.cjs +0 -178
|
@@ -153,16 +153,31 @@ function _setCapabilitySourceHttpGet(fn) {
|
|
|
153
153
|
// ---------------------------------------------------------------------------
|
|
154
154
|
// Helpers
|
|
155
155
|
// ---------------------------------------------------------------------------
|
|
156
|
-
/**
|
|
157
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the running GSD version; fail-closed to '0.0.0'.
|
|
158
|
+
*
|
|
159
|
+
* Prefer the authoritative `gsd-core/VERSION` the installer writes for EVERY runtime
|
|
160
|
+
* (libDir = gsd-core/bin/lib/, so `../../VERSION` = gsd-core/VERSION), so installed
|
|
161
|
+
* layouts report the true version even when the walked-up `../../../package.json` is
|
|
162
|
+
* absent or belongs to the user's own project (#1920). Fall back to the runtime-root
|
|
163
|
+
* package.json (dev/source tree), then fail-closed. Mirrors resolveVersionFrom() (#1383).
|
|
164
|
+
*/
|
|
165
|
+
function readHostVersion(libDir = __dirname) {
|
|
166
|
+
const SEMVER_PREFIX = /^\d+\.\d+\.\d+/;
|
|
158
167
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
const v = node_fs_1.default.readFileSync(node_path_1.default.join(libDir, '..', '..', 'VERSION'), 'utf8').trim();
|
|
169
|
+
if (SEMVER_PREFIX.test(v))
|
|
170
|
+
return v;
|
|
162
171
|
}
|
|
163
|
-
catch {
|
|
164
|
-
|
|
172
|
+
catch { /* not an installed tree (no gsd-core/VERSION) */ }
|
|
173
|
+
try {
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
175
|
+
const pkg = require(node_path_1.default.join(libDir, '..', '..', '..', 'package.json'));
|
|
176
|
+
if (pkg && typeof pkg.version === 'string' && SEMVER_PREFIX.test(pkg.version))
|
|
177
|
+
return pkg.version;
|
|
165
178
|
}
|
|
179
|
+
catch { /* runtime root has no package.json */ }
|
|
180
|
+
return '0.0.0';
|
|
166
181
|
}
|
|
167
182
|
/** Compute sha512-<base64> integrity over a buffer. */
|
|
168
183
|
function computeIntegrity(buf) {
|
|
@@ -594,13 +609,13 @@ function stageValidated(opts) {
|
|
|
594
609
|
// lives in capability-lifecycle.cjs and uses promote:false above.)
|
|
595
610
|
if (node_fs_1.default.existsSync(finalDir)) {
|
|
596
611
|
const backupDir = `${finalDir}.old-${process.pid}-${Date.now()}`;
|
|
597
|
-
|
|
612
|
+
shellSeam.retryRenameSync(finalDir, backupDir);
|
|
598
613
|
try {
|
|
599
|
-
|
|
614
|
+
shellSeam.retryRenameSync(stagingDir, finalDir);
|
|
600
615
|
}
|
|
601
616
|
catch (err) {
|
|
602
617
|
try {
|
|
603
|
-
|
|
618
|
+
shellSeam.retryRenameSync(backupDir, finalDir);
|
|
604
619
|
}
|
|
605
620
|
catch { /* best-effort restore */ }
|
|
606
621
|
throw err;
|
|
@@ -611,7 +626,7 @@ function stageValidated(opts) {
|
|
|
611
626
|
catch { /* best-effort */ }
|
|
612
627
|
}
|
|
613
628
|
else {
|
|
614
|
-
|
|
629
|
+
shellSeam.retryRenameSync(stagingDir, finalDir);
|
|
615
630
|
}
|
|
616
631
|
const version = typeof cap['version'] === 'string' ? cap['version'] : '';
|
|
617
632
|
return { id, version, stagedDir: finalDir, integrity, source };
|
|
@@ -704,7 +704,12 @@ const VALID_CONFIG_FORMATS = new Set(['settings-json', 'toml', 'markdown', 'mark
|
|
|
704
704
|
const VALID_CONFIG_HOME_KINDS = new Set(['dot-home', 'dot-home-nested', 'xdg', 'generic-agents-root']);
|
|
705
705
|
const VALID_COMMAND_STYLES = new Set(['slash-hyphen', 'shell-var']);
|
|
706
706
|
const VALID_HOOKS_SURFACES = new Set(['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'none']);
|
|
707
|
-
const VALID_HOOK_EVENTS = new Set(['claude', 'gemini'
|
|
707
|
+
const VALID_HOOK_EVENTS = new Set(['claude', 'gemini']);
|
|
708
|
+
// extensionEvents — the plugin/extension-system event dialect (ADR-1239 amendment / #1943).
|
|
709
|
+
// DISTINCT from hookEvents (managed-hook dialect): extensionEvents describes the
|
|
710
|
+
// plugin-owned event subset imperative hosts expose (opencode / pi); 'none' = the
|
|
711
|
+
// host exposes no extension surface (engine owns the bus, e.g. VS Code).
|
|
712
|
+
const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'none']);
|
|
708
713
|
const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
|
|
709
714
|
const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills', 'kimi-agents']);
|
|
710
715
|
const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']);
|
|
@@ -713,6 +718,16 @@ const VALID_INSTALL_SURFACES = new Set(['settings-json', 'codex-toml', 'copilot-
|
|
|
713
718
|
const VALID_PERMISSION_WRITERS = new Set(['opencode', 'kilo']);
|
|
714
719
|
const VALID_EXTENDED_HOOK_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'BeforeAgent', 'AfterAgent', 'BeforeModel']);
|
|
715
720
|
|
|
721
|
+
// ADR-1239 Phase A: hostIntegration axes (MUST stay parity-identical to HOST_INTEGRATION_AXES in src/host-integration.cts)
|
|
722
|
+
const VALID_EMBEDDING_MODES = new Set(['imperative', 'declarative']);
|
|
723
|
+
const VALID_COMMAND_SURFACES = new Set(['slash-file', 'slash-programmatic', 'slash-toml', 'palette', 'prose-only']);
|
|
724
|
+
const VALID_MODEL_MODES = new Set(['active', 'passive']);
|
|
725
|
+
const VALID_HOOK_BUSES = new Set(['host', 'engine', 'none']);
|
|
726
|
+
const VALID_STATE_IO = new Set(['filesystem', 'sandboxed-storage', 'session-log-append']);
|
|
727
|
+
const VALID_TRANSPORTS = new Set(['mcp', 'native-extension']);
|
|
728
|
+
const VALID_HOST_RUNTIMES = new Set(['node', 'bun', 'sandboxed-web', 'python', 'go', 'rust', 'electron', 'other']);
|
|
729
|
+
const VALID_SUBAGENT_TOOLKITS = new Set(['full', 'read-only']);
|
|
730
|
+
|
|
716
731
|
// GATE A: installSurface → allowed hooksSurface values (DEFECT.GENERATIVE-FIX: parity invariant)
|
|
717
732
|
// Derived from the actual pairings in the 16 real runtime descriptors.
|
|
718
733
|
const INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES = new Map([
|
|
@@ -968,7 +983,9 @@ function validateRuntimeBody(cap) {
|
|
|
968
983
|
);
|
|
969
984
|
}
|
|
970
985
|
|
|
971
|
-
// hookEvents — optional; if present must be in closed
|
|
986
|
+
// hookEvents — optional; if present must be in closed enum (ADR-1016 Decision 5).
|
|
987
|
+
// Managed-hook dialect only (claude/gemini). The OpenCode extension-system event
|
|
988
|
+
// subset is NOT a hookEvents value — it is `extensionEvents` (ADR-1239 / #1943).
|
|
972
989
|
if (r.hookEvents !== undefined) {
|
|
973
990
|
if (r.hookEvents === '__proto__' || r.hookEvents === 'constructor' || r.hookEvents === 'prototype') {
|
|
974
991
|
errors.push('runtime.hookEvents "' + r.hookEvents + '" is a reserved name');
|
|
@@ -980,6 +997,20 @@ function validateRuntimeBody(cap) {
|
|
|
980
997
|
}
|
|
981
998
|
}
|
|
982
999
|
|
|
1000
|
+
// extensionEvents — optional; the extension-system event dialect (ADR-1239 amendment / #1943).
|
|
1001
|
+
// Distinct from hookEvents. Only imperative-embedding hosts (with a plugin/extension
|
|
1002
|
+
// API) set it; declarative hosts do not.
|
|
1003
|
+
if (r.extensionEvents !== undefined) {
|
|
1004
|
+
if (r.extensionEvents === '__proto__' || r.extensionEvents === 'constructor' || r.extensionEvents === 'prototype') {
|
|
1005
|
+
errors.push('runtime.extensionEvents "' + r.extensionEvents + '" is a reserved name');
|
|
1006
|
+
} else if (!VALID_EXTENSION_EVENTS.has(r.extensionEvents)) {
|
|
1007
|
+
errors.push(
|
|
1008
|
+
'runtime.extensionEvents must be one of: ' + [...VALID_EXTENSION_EVENTS].join(', ') +
|
|
1009
|
+
' (got: ' + JSON.stringify(r.extensionEvents) + ')',
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
983
1014
|
// sandboxTier — closed 2-enum (ADR-1016 Decision 6); inline literal guard (CodeQL barrier)
|
|
984
1015
|
if (r.sandboxTier === '__proto__' || r.sandboxTier === 'constructor' || r.sandboxTier === 'prototype') {
|
|
985
1016
|
errors.push('runtime.sandboxTier "' + r.sandboxTier + '" is a reserved name');
|
|
@@ -1020,6 +1051,20 @@ function validateRuntimeBody(cap) {
|
|
|
1020
1051
|
);
|
|
1021
1052
|
}
|
|
1022
1053
|
|
|
1054
|
+
// localConfigDir — REQUIRED non-empty dot-dir string (ADR-1239 Phase B #1679)
|
|
1055
|
+
// Must start with '.' (e.g. ".claude", ".cursor"). Validated here so the registry
|
|
1056
|
+
// generator catches any descriptor missing the field before regenerating.
|
|
1057
|
+
if (typeof r.localConfigDir !== 'string' || r.localConfigDir.length === 0) {
|
|
1058
|
+
errors.push(
|
|
1059
|
+
'runtime.localConfigDir is required and must be a non-empty string (e.g. ".claude"); ' +
|
|
1060
|
+
'got: ' + JSON.stringify(r.localConfigDir),
|
|
1061
|
+
);
|
|
1062
|
+
} else if (!r.localConfigDir.startsWith('.')) {
|
|
1063
|
+
errors.push(
|
|
1064
|
+
'runtime.localConfigDir must start with "." (a dot-dir); got: ' + JSON.stringify(r.localConfigDir),
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1023
1068
|
// extendedHookEvents — required array; every element must be in closed enum
|
|
1024
1069
|
if (!Array.isArray(r.extendedHookEvents)) {
|
|
1025
1070
|
errors.push(
|
|
@@ -1037,6 +1082,162 @@ function validateRuntimeBody(cap) {
|
|
|
1037
1082
|
}
|
|
1038
1083
|
}
|
|
1039
1084
|
|
|
1085
|
+
// hostIntegration — ADR-1239 Phase A: required object with closed-enum axes
|
|
1086
|
+
if (typeof r.hostIntegration !== 'object' || r.hostIntegration === null || Array.isArray(r.hostIntegration)) {
|
|
1087
|
+
errors.push('runtime.hostIntegration is required and must be an object');
|
|
1088
|
+
} else {
|
|
1089
|
+
const hi = r.hostIntegration;
|
|
1090
|
+
|
|
1091
|
+
// S2b: reserved-OWN-KEY guard on hostIntegration (CodeQL barrier — inline literal comparisons)
|
|
1092
|
+
if (Object.prototype.hasOwnProperty.call(hi, '__proto__')) {
|
|
1093
|
+
errors.push('runtime.hostIntegration must not contain reserved key "__proto__"');
|
|
1094
|
+
}
|
|
1095
|
+
if (Object.prototype.hasOwnProperty.call(hi, 'constructor')) {
|
|
1096
|
+
errors.push('runtime.hostIntegration must not contain reserved key "constructor"');
|
|
1097
|
+
}
|
|
1098
|
+
if (Object.prototype.hasOwnProperty.call(hi, 'prototype')) {
|
|
1099
|
+
errors.push('runtime.hostIntegration must not contain reserved key "prototype"');
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// embeddingMode
|
|
1103
|
+
if (hi.embeddingMode === '__proto__' || hi.embeddingMode === 'constructor' || hi.embeddingMode === 'prototype') {
|
|
1104
|
+
errors.push('runtime.hostIntegration.embeddingMode "' + hi.embeddingMode + '" is a reserved name');
|
|
1105
|
+
} else if (hi.embeddingMode !== 'undocumented' && !VALID_EMBEDDING_MODES.has(hi.embeddingMode)) {
|
|
1106
|
+
errors.push(
|
|
1107
|
+
'runtime.hostIntegration.embeddingMode must be one of: ' + [...VALID_EMBEDDING_MODES].join(', ') +
|
|
1108
|
+
' (or "undocumented") (got: ' + JSON.stringify(hi.embeddingMode) + ')',
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// commandSurface
|
|
1113
|
+
if (hi.commandSurface === '__proto__' || hi.commandSurface === 'constructor' || hi.commandSurface === 'prototype') {
|
|
1114
|
+
errors.push('runtime.hostIntegration.commandSurface "' + hi.commandSurface + '" is a reserved name');
|
|
1115
|
+
} else if (hi.commandSurface !== 'undocumented' && !VALID_COMMAND_SURFACES.has(hi.commandSurface)) {
|
|
1116
|
+
errors.push(
|
|
1117
|
+
'runtime.hostIntegration.commandSurface must be one of: ' + [...VALID_COMMAND_SURFACES].join(', ') +
|
|
1118
|
+
' (or "undocumented") (got: ' + JSON.stringify(hi.commandSurface) + ')',
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// modelMode
|
|
1123
|
+
if (hi.modelMode === '__proto__' || hi.modelMode === 'constructor' || hi.modelMode === 'prototype') {
|
|
1124
|
+
errors.push('runtime.hostIntegration.modelMode "' + hi.modelMode + '" is a reserved name');
|
|
1125
|
+
} else if (hi.modelMode !== 'undocumented' && !VALID_MODEL_MODES.has(hi.modelMode)) {
|
|
1126
|
+
errors.push(
|
|
1127
|
+
'runtime.hostIntegration.modelMode must be one of: ' + [...VALID_MODEL_MODES].join(', ') +
|
|
1128
|
+
' (or "undocumented") (got: ' + JSON.stringify(hi.modelMode) + ')',
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// hookBus
|
|
1133
|
+
if (hi.hookBus === '__proto__' || hi.hookBus === 'constructor' || hi.hookBus === 'prototype') {
|
|
1134
|
+
errors.push('runtime.hostIntegration.hookBus "' + hi.hookBus + '" is a reserved name');
|
|
1135
|
+
} else if (hi.hookBus !== 'undocumented' && !VALID_HOOK_BUSES.has(hi.hookBus)) {
|
|
1136
|
+
errors.push(
|
|
1137
|
+
'runtime.hostIntegration.hookBus must be one of: ' + [...VALID_HOOK_BUSES].join(', ') +
|
|
1138
|
+
' (or "undocumented") (got: ' + JSON.stringify(hi.hookBus) + ')',
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// stateIO
|
|
1143
|
+
if (hi.stateIO === '__proto__' || hi.stateIO === 'constructor' || hi.stateIO === 'prototype') {
|
|
1144
|
+
errors.push('runtime.hostIntegration.stateIO "' + hi.stateIO + '" is a reserved name');
|
|
1145
|
+
} else if (hi.stateIO !== 'undocumented' && !VALID_STATE_IO.has(hi.stateIO)) {
|
|
1146
|
+
errors.push(
|
|
1147
|
+
'runtime.hostIntegration.stateIO must be one of: ' + [...VALID_STATE_IO].join(', ') +
|
|
1148
|
+
' (or "undocumented") (got: ' + JSON.stringify(hi.stateIO) + ')',
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// transport
|
|
1153
|
+
if (hi.transport === '__proto__' || hi.transport === 'constructor' || hi.transport === 'prototype') {
|
|
1154
|
+
errors.push('runtime.hostIntegration.transport "' + hi.transport + '" is a reserved name');
|
|
1155
|
+
} else if (hi.transport !== 'undocumented' && !VALID_TRANSPORTS.has(hi.transport)) {
|
|
1156
|
+
errors.push(
|
|
1157
|
+
'runtime.hostIntegration.transport must be one of: ' + [...VALID_TRANSPORTS].join(', ') +
|
|
1158
|
+
' (or "undocumented") (got: ' + JSON.stringify(hi.transport) + ')',
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// runtime (axis)
|
|
1163
|
+
if (hi.runtime === '__proto__' || hi.runtime === 'constructor' || hi.runtime === 'prototype') {
|
|
1164
|
+
errors.push('runtime.hostIntegration.runtime "' + hi.runtime + '" is a reserved name');
|
|
1165
|
+
} else if (hi.runtime !== 'undocumented' && !VALID_HOST_RUNTIMES.has(hi.runtime)) {
|
|
1166
|
+
errors.push(
|
|
1167
|
+
'runtime.hostIntegration.runtime must be one of: ' + [...VALID_HOST_RUNTIMES].join(', ') +
|
|
1168
|
+
' (or "undocumented") (got: ' + JSON.stringify(hi.runtime) + ')',
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// dispatch — required object
|
|
1173
|
+
if (typeof hi.dispatch !== 'object' || hi.dispatch === null || Array.isArray(hi.dispatch)) {
|
|
1174
|
+
errors.push('runtime.hostIntegration.dispatch must be an object');
|
|
1175
|
+
} else {
|
|
1176
|
+
const d = hi.dispatch;
|
|
1177
|
+
|
|
1178
|
+
// S2b: reserved-OWN-KEY guard on dispatch (CodeQL barrier — inline literal comparisons)
|
|
1179
|
+
if (Object.prototype.hasOwnProperty.call(d, '__proto__')) {
|
|
1180
|
+
errors.push('runtime.hostIntegration.dispatch must not contain reserved key "__proto__"');
|
|
1181
|
+
}
|
|
1182
|
+
if (Object.prototype.hasOwnProperty.call(d, 'constructor')) {
|
|
1183
|
+
errors.push('runtime.hostIntegration.dispatch must not contain reserved key "constructor"');
|
|
1184
|
+
}
|
|
1185
|
+
if (Object.prototype.hasOwnProperty.call(d, 'prototype')) {
|
|
1186
|
+
errors.push('runtime.hostIntegration.dispatch must not contain reserved key "prototype"');
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// namedDispatch — boolean or 'undocumented'
|
|
1190
|
+
if (typeof d.namedDispatch !== 'boolean' && d.namedDispatch !== 'undocumented') {
|
|
1191
|
+
errors.push(
|
|
1192
|
+
'runtime.hostIntegration.dispatch.namedDispatch must be a boolean or "undocumented" (got: ' + JSON.stringify(d.namedDispatch) + ')',
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// nested — boolean or 'undocumented'
|
|
1197
|
+
if (typeof d.nested !== 'boolean' && d.nested !== 'undocumented') {
|
|
1198
|
+
errors.push(
|
|
1199
|
+
'runtime.hostIntegration.dispatch.nested must be a boolean or "undocumented" (got: ' + JSON.stringify(d.nested) + ')',
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// background — boolean or 'undocumented'
|
|
1204
|
+
if (typeof d.background !== 'boolean' && d.background !== 'undocumented') {
|
|
1205
|
+
errors.push(
|
|
1206
|
+
'runtime.hostIntegration.dispatch.background must be a boolean or "undocumented" (got: ' + JSON.stringify(d.background) + ')',
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// subagentToolkit — closed enum or 'undocumented'
|
|
1211
|
+
if (d.subagentToolkit === '__proto__' || d.subagentToolkit === 'constructor' || d.subagentToolkit === 'prototype') {
|
|
1212
|
+
errors.push('runtime.hostIntegration.dispatch.subagentToolkit "' + d.subagentToolkit + '" is a reserved name');
|
|
1213
|
+
} else if (d.subagentToolkit !== 'undocumented' && !VALID_SUBAGENT_TOOLKITS.has(d.subagentToolkit)) {
|
|
1214
|
+
errors.push(
|
|
1215
|
+
'runtime.hostIntegration.dispatch.subagentToolkit must be one of: ' + [...VALID_SUBAGENT_TOOLKITS].join(', ') +
|
|
1216
|
+
' (or "undocumented") (got: ' + JSON.stringify(d.subagentToolkit) + ')',
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// maxDepth — integer >= -1 or 'undocumented'
|
|
1221
|
+
if (d.maxDepth !== 'undocumented' && (!Number.isInteger(d.maxDepth) || d.maxDepth < -1)) {
|
|
1222
|
+
errors.push(
|
|
1223
|
+
'runtime.hostIntegration.dispatch.maxDepth must be an integer >= -1 or "undocumented" (got: ' + JSON.stringify(d.maxDepth) + ')',
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// backgroundDispatch — REQUIRED (all 16 runtime descriptors carry it, matching the sibling fields
|
|
1228
|
+
// namedDispatch/nested/background/subagentToolkit/maxDepth which are all required).
|
|
1229
|
+
if (!Object.prototype.hasOwnProperty.call(d, 'backgroundDispatch')) {
|
|
1230
|
+
errors.push(
|
|
1231
|
+
'runtime.hostIntegration.dispatch.backgroundDispatch is required (must be a boolean or "undocumented")',
|
|
1232
|
+
);
|
|
1233
|
+
} else if (typeof d.backgroundDispatch !== 'boolean' && d.backgroundDispatch !== 'undocumented') {
|
|
1234
|
+
errors.push(
|
|
1235
|
+
'runtime.hostIntegration.dispatch.backgroundDispatch must be a boolean or "undocumented" (got: ' + JSON.stringify(d.backgroundDispatch) + ')',
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1040
1241
|
// GATE A: installSurface ↔ hooksSurface consistency (DEFECT.GENERATIVE-FIX)
|
|
1041
1242
|
// Only check if both fields are valid strings (individual field validators above report type errors).
|
|
1042
1243
|
if (typeof r.installSurface === 'string' && typeof r.hooksSurface === 'string') {
|
|
@@ -2045,6 +2246,7 @@ module.exports = {
|
|
|
2045
2246
|
VALID_COMMAND_STYLES,
|
|
2046
2247
|
VALID_HOOKS_SURFACES,
|
|
2047
2248
|
VALID_HOOK_EVENTS,
|
|
2249
|
+
VALID_EXTENSION_EVENTS,
|
|
2048
2250
|
VALID_SANDBOX_TIERS,
|
|
2049
2251
|
VALID_ARTIFACT_KIND_NAMES,
|
|
2050
2252
|
VALID_ARTIFACT_NESTINGS,
|
|
@@ -2052,6 +2254,24 @@ module.exports = {
|
|
|
2052
2254
|
VALID_INSTALL_SURFACES,
|
|
2053
2255
|
VALID_PERMISSION_WRITERS,
|
|
2054
2256
|
VALID_EXTENDED_HOOK_EVENTS,
|
|
2257
|
+
VALID_EMBEDDING_MODES,
|
|
2258
|
+
VALID_COMMAND_SURFACES,
|
|
2259
|
+
VALID_MODEL_MODES,
|
|
2260
|
+
VALID_HOOK_BUSES,
|
|
2261
|
+
VALID_STATE_IO,
|
|
2262
|
+
VALID_TRANSPORTS,
|
|
2263
|
+
VALID_HOST_RUNTIMES,
|
|
2264
|
+
VALID_SUBAGENT_TOOLKITS,
|
|
2265
|
+
_HOST_INTEGRATION_VOCAB: {
|
|
2266
|
+
embeddingMode: [...VALID_EMBEDDING_MODES],
|
|
2267
|
+
commandSurface: [...VALID_COMMAND_SURFACES],
|
|
2268
|
+
modelMode: [...VALID_MODEL_MODES],
|
|
2269
|
+
hookBus: [...VALID_HOOK_BUSES],
|
|
2270
|
+
stateIO: [...VALID_STATE_IO],
|
|
2271
|
+
transport: [...VALID_TRANSPORTS],
|
|
2272
|
+
runtime: [...VALID_HOST_RUNTIMES],
|
|
2273
|
+
subagentToolkit: [...VALID_SUBAGENT_TOOLKITS],
|
|
2274
|
+
},
|
|
2055
2275
|
INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES,
|
|
2056
2276
|
GEMINI_AGENT_EVENTS,
|
|
2057
2277
|
CLAUDE_FAMILY_EVENTS,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.checkCliSkew = checkCliSkew;
|
|
7
|
+
/**
|
|
8
|
+
* cli-skew-check.cts — CLI version-skew detection (#1754).
|
|
9
|
+
*
|
|
10
|
+
* Pure function: compares the resolved gsd-tools.cjs path to the project root.
|
|
11
|
+
* If the resolved CLI is OUTSIDE the project root while a project-local install
|
|
12
|
+
* EXISTS, returns a warning string (the caller writes it to stderr). Non-blocking.
|
|
13
|
+
*
|
|
14
|
+
* Catches the shadowing scenario from #1748: a stale global canary CLI (e.g.
|
|
15
|
+
* from the retired @gsd-build/sdk) shadowing the project-local GSD install.
|
|
16
|
+
*
|
|
17
|
+
* The function is PURE (no I/O) — the caller provides the resolved path, the
|
|
18
|
+
* project root, and whether a project-local install exists. This makes it
|
|
19
|
+
* trivially testable without filesystem setup.
|
|
20
|
+
*/
|
|
21
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
/**
|
|
23
|
+
* Check for CLI version skew.
|
|
24
|
+
*
|
|
25
|
+
* @param opts.resolvedPath - The absolute path of the running gsd-tools.cjs (__filename).
|
|
26
|
+
* @param opts.projectRoot - The project root (from findProjectRoot), or null if no project.
|
|
27
|
+
* @param opts.projectLocalExists - Whether a project-local gsd-tools.cjs exists.
|
|
28
|
+
* @returns A warning string if skew is detected, or null if no skew.
|
|
29
|
+
*/
|
|
30
|
+
function checkCliSkew(opts) {
|
|
31
|
+
const { resolvedPath, projectRoot, projectLocalExists } = opts;
|
|
32
|
+
// No project context or no project-local install → no skew possible.
|
|
33
|
+
if (!projectRoot || !projectLocalExists)
|
|
34
|
+
return null;
|
|
35
|
+
// If the resolved CLI is under the project root, it IS a project-local install.
|
|
36
|
+
const rel = node_path_1.default.relative(projectRoot, resolvedPath);
|
|
37
|
+
if (!rel.startsWith('..'))
|
|
38
|
+
return null;
|
|
39
|
+
// Resolved CLI is outside project root while a project-local install exists → SKEW.
|
|
40
|
+
const hint = resolvedPath.includes('@gsd-build')
|
|
41
|
+
? ' If @gsd-build/sdk: npm uninstall -g @gsd-build/sdk'
|
|
42
|
+
: '';
|
|
43
|
+
return `⚠ GSD: ${resolvedPath} may shadow project-local GSD.${hint}`;
|
|
44
|
+
}
|
|
@@ -159,6 +159,14 @@ exports.STATE_COMMAND_ALIASES = [
|
|
|
159
159
|
"subcommand": "prune",
|
|
160
160
|
"mutation": true
|
|
161
161
|
},
|
|
162
|
+
{
|
|
163
|
+
"canonical": "state.rebuild",
|
|
164
|
+
"aliases": [
|
|
165
|
+
"state rebuild"
|
|
166
|
+
],
|
|
167
|
+
"subcommand": "rebuild",
|
|
168
|
+
"mutation": true
|
|
169
|
+
},
|
|
162
170
|
{
|
|
163
171
|
"canonical": "state.milestone-switch",
|
|
164
172
|
"aliases": [
|
|
@@ -1302,7 +1302,8 @@ function cmdStats(cwd, format, raw) {
|
|
|
1302
1302
|
const roadmapContent = extractCurrentMilestone(roadmapRaw, cwd);
|
|
1303
1303
|
// Matches both plain numeric (Phase 1:) and milestone-prefixed (Phase 2-01:) headings.
|
|
1304
1304
|
// Also tolerates optional [bracket-token] scope prefix on phase headings.
|
|
1305
|
-
|
|
1305
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1306
|
+
const headingPattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
|
|
1306
1307
|
let match;
|
|
1307
1308
|
while ((match = headingPattern.exec(roadmapContent)) !== null) {
|
|
1308
1309
|
const key = normalizePhaseName(match[1]);
|
|
@@ -637,6 +637,33 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
637
637
|
}
|
|
638
638
|
parsedValue = normalized.values;
|
|
639
639
|
}
|
|
640
|
+
// #1517: validate review.reviewer_instances.<name>.<field> leaves at the
|
|
641
|
+
// invocation boundary (Postel/Kerckhoffs — strict at accept). The config
|
|
642
|
+
// schema dynamic pattern admits the path; this block validates the name + the
|
|
643
|
+
// field value so a misconfigured instance is rejected at config-set time, not
|
|
644
|
+
// silently at review time. Single-source validators live in
|
|
645
|
+
// review-reviewer-selection.cjs (INSTANCE_NAME_PATTERN, KNOWN_REVIEWER_SLUGS).
|
|
646
|
+
const instanceLeaf = kp.match(/^review\.reviewer_instances\.([a-zA-Z0-9_-]+)\.(cli|model|agent)$/);
|
|
647
|
+
if (instanceLeaf) {
|
|
648
|
+
const [, instanceName, field] = instanceLeaf;
|
|
649
|
+
if (!review_reviewer_selection_cjs_1.INSTANCE_NAME_PATTERN.test(instanceName)) {
|
|
650
|
+
error(`Invalid reviewer instance name '${instanceName}'. Must match ^[a-z0-9][a-z0-9-]*$.`);
|
|
651
|
+
}
|
|
652
|
+
if (review_reviewer_selection_cjs_1.KNOWN_REVIEWER_SLUGS.includes(instanceName)) {
|
|
653
|
+
error(`Reviewer instance name '${instanceName}' must not equal a built-in reviewer slug.`);
|
|
654
|
+
}
|
|
655
|
+
if (field === 'cli') {
|
|
656
|
+
if (typeof parsedValue !== 'string' || !review_reviewer_selection_cjs_1.KNOWN_REVIEWER_SLUGS.includes(parsedValue)) {
|
|
657
|
+
error(`Invalid reviewer_instances.${instanceName}.cli '${val}'. Must be a known reviewer adapter: ${review_reviewer_selection_cjs_1.KNOWN_REVIEWER_SLUGS.join(', ')}.`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
// model | agent — opaque pass-through strings (never interpolated into shell).
|
|
662
|
+
if (typeof parsedValue !== 'string') {
|
|
663
|
+
error(`Invalid reviewer_instances.${instanceName}.${field} '${val}'. Must be a string.`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
640
667
|
const setConfigValueResult = setConfigValue(cwd, kp, parsedValue);
|
|
641
668
|
// Mask secrets in both JSON and text output. The plaintext is written
|
|
642
669
|
// to config.json (that's where secrets live on disk); the CLI output
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedding adapter contract — the common `HostIntegrationInterface` both
|
|
3
|
+
* embedding adapters satisfy (ADR-1239 Phase C-1, #1680).
|
|
4
|
+
*
|
|
5
|
+
* INTENTIONALLY MINIMAL (Phase 3 slice 1). The full six-interface-point binding
|
|
6
|
+
* surface (command / dispatch / model / hooks / state / artifact) is DEFERRED
|
|
7
|
+
* until the imperative adapter (AC2) provides a real consumer that fixes the
|
|
8
|
+
* shape — ADR-1239 lists the wire-shape as an open question:
|
|
9
|
+
* "Exact wire-shape of the initialize handshake … Where precisely to cut the
|
|
10
|
+
* engine↔host boundary"
|
|
11
|
+
* (docs/adr/1239-gsd-embeddable-orchestration-engine.md#open-questions-narrowed-by-the-research).
|
|
12
|
+
* Freezing a 6-point contract before the imperative adapter exists would risk
|
|
13
|
+
* rework across Phases 3-6. This slice ships only what the declarative adapter
|
|
14
|
+
* (AC1) needs: the kind discriminator + runtime + install/uninstall entry.
|
|
15
|
+
*
|
|
16
|
+
* Both adapters bind the SAME engine (install-engine.cjs / the loop resolver);
|
|
17
|
+
* they differ in HOW — declarative projects files (lossy: drops loop
|
|
18
|
+
* orchestration), imperative drives host primitives in-process. See ADR-1239
|
|
19
|
+
* "How a capability reaches a host (two adapters, one engine)".
|
|
20
|
+
*/
|
|
21
|
+
'use strict';
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.ADAPTER_KINDS = void 0;
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Kinds
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
exports.ADAPTER_KINDS = Object.freeze(['declarative', 'imperative']);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External-descriptor trust gate (ADR-1239 Phase C-2, #1681).
|
|
3
|
+
*
|
|
4
|
+
* Load-time `configHome` write-confinement for installed third-party host-plugin
|
|
5
|
+
* descriptors. The opt-in loader (`loadRegistry({includeInstalled:true})`) already
|
|
6
|
+
* applies schema validation + consent + first-party-wins + fail-closed gates;
|
|
7
|
+
* this adds defense-in-depth: **before** a third-party descriptor's install plan
|
|
8
|
+
* is ever executed, assert every destSubpath it declares resolves within the
|
|
9
|
+
* user-approved `configHome`. A path-escaping or malformed descriptor is
|
|
10
|
+
* rejected fail-closed.
|
|
11
|
+
*
|
|
12
|
+
* This is the load-time twin of Phase 2's install-time gate
|
|
13
|
+
* (`assertDestWithinConfigHome` in runtime-artifact-install-plan.cts, #1679 AC3).
|
|
14
|
+
* The two are defense-in-depth: load-time rejects malformed descriptors early
|
|
15
|
+
* (before consent even matters); install-time bounds the actual writes.
|
|
16
|
+
*
|
|
17
|
+
* Do NOT conflate with ADR-1577's prompt-injection circuit-breaker — separate
|
|
18
|
+
* concern sharing the word "trust".
|
|
19
|
+
*/
|
|
20
|
+
'use strict';
|
|
21
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
22
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.isPathConfined = isPathConfined;
|
|
26
|
+
exports.assertDescriptorConfined = assertDescriptorConfined;
|
|
27
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
28
|
+
/**
|
|
29
|
+
* Pure path-containment check (cross-platform). `target` is confined to `root`
|
|
30
|
+
* iff resolving it relative to `root` yields a path equal to or under `root`.
|
|
31
|
+
* Absolute paths outside `root` and `..`-escapes return false.
|
|
32
|
+
*/
|
|
33
|
+
function isPathConfined(target, root) {
|
|
34
|
+
if (typeof target !== 'string' || typeof root !== 'string' || target.length === 0 || root.length === 0) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const rootResolved = node_path_1.default.resolve(root);
|
|
38
|
+
const targetResolved = node_path_1.default.resolve(root, target);
|
|
39
|
+
const prefix = rootResolved + node_path_1.default.sep;
|
|
40
|
+
return targetResolved === rootResolved || targetResolved.startsWith(prefix);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Assert every destSubpath the descriptor declares (global + local artifact
|
|
44
|
+
* layout) resolves within `configHome`. Throws fail-closed naming the offending
|
|
45
|
+
* descriptor + path on the first escape. A descriptor with no artifact layout
|
|
46
|
+
* passes (nothing to confine).
|
|
47
|
+
*/
|
|
48
|
+
function assertDescriptorConfined(descriptor, configHome) {
|
|
49
|
+
if (!descriptor || typeof descriptor !== 'object')
|
|
50
|
+
return;
|
|
51
|
+
const id = typeof descriptor.id === 'string' ? descriptor.id : '<unknown>';
|
|
52
|
+
const layout = descriptor.runtime?.artifactLayout;
|
|
53
|
+
if (!layout || typeof layout !== 'object')
|
|
54
|
+
return;
|
|
55
|
+
const check = (scope, kinds) => {
|
|
56
|
+
if (!Array.isArray(kinds))
|
|
57
|
+
return;
|
|
58
|
+
for (const kind of kinds) {
|
|
59
|
+
const dest = kind?.destSubpath;
|
|
60
|
+
if (typeof dest !== 'string' || dest.length === 0)
|
|
61
|
+
continue;
|
|
62
|
+
if (!isPathConfined(dest, configHome)) {
|
|
63
|
+
throw new Error(`external-descriptor-trust: descriptor '${id}' declares an unconfined ${scope} destSubpath ` +
|
|
64
|
+
`${JSON.stringify(dest)} (resolves outside configHome ${JSON.stringify(configHome)}) — rejected fail-closed.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
check('global', layout.global);
|
|
69
|
+
check('local', layout.local);
|
|
70
|
+
}
|
|
@@ -126,6 +126,49 @@ function extractFrontmatter(content) {
|
|
|
126
126
|
}
|
|
127
127
|
return frontmatter;
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Escape a string for emission inside a YAML double-quoted scalar (#1779).
|
|
131
|
+
* Backslash must be escaped first so the backslashes added for embedded quotes
|
|
132
|
+
* (and control chars) are not themselves doubled. Without this, a value
|
|
133
|
+
* carrying an indicator (`:`/`#`) that also contains a literal `"` serializes
|
|
134
|
+
* to invalid YAML, e.g. `upstream: "https://x (Tom; "Git. Ship. Done")"`. A
|
|
135
|
+
* literal newline/tab/control char inside the quotes likewise breaks (or
|
|
136
|
+
* silently alters) the scalar, so those are escaped to their YAML forms too.
|
|
137
|
+
*/
|
|
138
|
+
function escapeDoubleQuoted(s) {
|
|
139
|
+
return s
|
|
140
|
+
.replace(/\\/g, '\\\\')
|
|
141
|
+
.replace(/"/g, '\\"')
|
|
142
|
+
.replace(/\n/g, '\\n')
|
|
143
|
+
.replace(/\t/g, '\\t')
|
|
144
|
+
.replace(/\r/g, '\\r')
|
|
145
|
+
// Remaining C0 controls + DEL → \xHH (a valid YAML double-quoted escape).
|
|
146
|
+
.replace(/[\u0000-\u001f\u007f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A plain (unquoted) scalar that would mis-parse or round-trip lossily when
|
|
150
|
+
* emitted bare must instead go through the double-quoted + escaped form
|
|
151
|
+
* (#1779): the empty string (bare `k:` reloads as null), an embedded `"`/`\`
|
|
152
|
+
* or control char, a leading YAML indicator (quote, `&`/`*`/`!` anchor/alias/
|
|
153
|
+
* tag, `|`/`>` block scalar, flow `[]{},`, `#`, reserved `%`/`@`/backtick, or
|
|
154
|
+
* `-`/`?`/`:` before a space), or leading/trailing whitespace. This helper is
|
|
155
|
+
* the correctness complement of `escapeDoubleQuoted`: it broadens the *trigger*
|
|
156
|
+
* for quoting without broadening the lossy object-list handling deferred to
|
|
157
|
+
* #1572/#1660.
|
|
158
|
+
*/
|
|
159
|
+
function scalarNeedsDoubleQuoting(s) {
|
|
160
|
+
if (s === '')
|
|
161
|
+
return true;
|
|
162
|
+
if (/["\\\u0000-\u001f\u007f]/.test(s))
|
|
163
|
+
return true;
|
|
164
|
+
// Always-unsafe leading indicators, or leading/trailing whitespace.
|
|
165
|
+
if (/^[,[\]{}#&*!|>'"%@`]/.test(s) || /^\s|\s$/.test(s))
|
|
166
|
+
return true;
|
|
167
|
+
// `-` `?` `:` only start a plain scalar safely when NOT followed by a space.
|
|
168
|
+
if (/^[-?:](\s|$)/.test(s))
|
|
169
|
+
return true;
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
129
172
|
function reconstructFrontmatter(obj) {
|
|
130
173
|
const lines = [];
|
|
131
174
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -141,7 +184,7 @@ function reconstructFrontmatter(obj) {
|
|
|
141
184
|
else {
|
|
142
185
|
lines.push(`${key}:`);
|
|
143
186
|
for (const item of value) {
|
|
144
|
-
lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
|
|
187
|
+
lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#') || scalarNeedsDoubleQuoting(item)) ? `"${escapeDoubleQuoted(item)}"` : item}`);
|
|
145
188
|
}
|
|
146
189
|
}
|
|
147
190
|
}
|
|
@@ -160,7 +203,7 @@ function reconstructFrontmatter(obj) {
|
|
|
160
203
|
else {
|
|
161
204
|
lines.push(` ${subkey}:`);
|
|
162
205
|
for (const item of subval) {
|
|
163
|
-
lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
|
|
206
|
+
lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#') || scalarNeedsDoubleQuoting(item)) ? `"${escapeDoubleQuoted(item)}"` : item}`);
|
|
164
207
|
}
|
|
165
208
|
}
|
|
166
209
|
}
|
|
@@ -189,14 +232,14 @@ function reconstructFrontmatter(obj) {
|
|
|
189
232
|
else {
|
|
190
233
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
191
234
|
const sv = String(subval);
|
|
192
|
-
lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') ? `"${sv}"` : sv}`);
|
|
235
|
+
lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') || scalarNeedsDoubleQuoting(sv) ? `"${escapeDoubleQuoted(sv)}"` : sv}`);
|
|
193
236
|
}
|
|
194
237
|
}
|
|
195
238
|
}
|
|
196
239
|
else {
|
|
197
240
|
const sv = String(value);
|
|
198
|
-
if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{')) {
|
|
199
|
-
lines.push(`${key}: "${sv}"`);
|
|
241
|
+
if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{') || scalarNeedsDoubleQuoting(sv)) {
|
|
242
|
+
lines.push(`${key}: "${escapeDoubleQuoted(sv)}"`);
|
|
200
243
|
}
|
|
201
244
|
else {
|
|
202
245
|
lines.push(`${key}: ${sv}`);
|
|
@@ -448,7 +491,11 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
448
491
|
else {
|
|
449
492
|
const kvMatch = trimmed.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
|
|
450
493
|
if (kvMatch) {
|
|
451
|
-
|
|
494
|
+
// Trim: a quoted value like `"backstop "` captures the inner trailing space in group 2.
|
|
495
|
+
// Left untrimmed, a hand-authored `must_haves` marker degrades (a `backstop` truth silently
|
|
496
|
+
// grades green instead of abstaining — #1905, the #1154 false-pass; also the sibling
|
|
497
|
+
// check_target/violationFixture path). Whitespace is never semantic in a scalar KV value.
|
|
498
|
+
const val = kvMatch[2].trim();
|
|
452
499
|
// Try to parse as number
|
|
453
500
|
(current)[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
|
|
454
501
|
}
|