fix(#3514): deny internal fetch hosts; disclose unverified integrity (#3516)

* test(#3514): add failing-first denylist and integrity suites

* fix(#3514): deny internal fetch hosts; disclose unverified integrity

* docs(#3514): trust-model, glossary, and changeset entries

* fix(#3514): scope v6 checks to literals; exact pin kinds in prompt

* chore(#3514): backfill changeset pr number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-14 21:34:29 -04:00
committed by GitHub
parent 268ca7e32d
commit 6badb839a0
18 changed files with 842 additions and 602 deletions

View File

@@ -616,6 +616,88 @@ test('TV-09: signatureForManifest does NOT vary with missingArtifacts (MISSING a
}
});
// ---------------------------------------------------------------------------
// #3514 (epic #1900 F21c) — unverified-integrity disclosure. The verdict gains
// integrityPinned; the Disclosure carries integrityStatus ('pinned' |
// 'unverified'); summarizeDisclosure renders it. Deliberately NOT part of the
// consent-binding signature — consent's content binding is bundleContentHash
// (#1459), and a prompt line must never force a spurious re-consent.
// ---------------------------------------------------------------------------
const EXEC_MANIFEST_3514 = {
id: 'integrity-status-cap',
hooks: [{ event: 'PostToolUse', script: 'hooks/run.js' }],
};
test('#3514: integrityPinned:true verdict carries integrityStatus "pinned" and renders the pinned line', () => {
const v = trust.evaluateInstallTrust({
parsed: { kind: 'tarball', raw: 'https://example.com/cap.tgz', target: 'https://example.com/cap.tgz' },
manifest: EXEC_MANIFEST_3514,
hostVersion: '1.6.0',
integrityPin: 'sha512',
});
assert.strictEqual(v.disclosure.integrityStatus, 'pinned');
const joined = trust.summarizeDisclosure(v.disclosure).join('\n');
assert.match(joined, /pin supplied and verified/i);
assert.doesNotMatch(joined, /NO PINNED HASH/i);
});
test('#3514: integrityPinned:false verdict carries integrityStatus "unverified" and renders the unverified line', () => {
const v = trust.evaluateInstallTrust({
parsed: { kind: 'tarball', raw: 'https://example.com/cap.tgz', target: 'https://example.com/cap.tgz' },
manifest: EXEC_MANIFEST_3514,
hostVersion: '1.6.0',
integrityPin: 'none',
});
assert.strictEqual(v.disclosure.integrityStatus, 'unverified');
const joined = trust.summarizeDisclosure(v.disclosure).join('\n');
assert.match(joined, /NO PINNED HASH/i);
assert.match(joined, /staged unverified/i);
});
test('#3514: integrityPin git-commit renders the commit-pinned line, never a sha512 claim', () => {
const v = trust.evaluateInstallTrust({
parsed: { kind: 'git', raw: 'https://example.com/cap.git#sha:0123456789abcdef0123456789abcdef01234567', target: 'https://example.com/cap.git' },
manifest: EXEC_MANIFEST_3514,
hostVersion: '1.6.0',
integrityPin: 'git-commit',
});
assert.strictEqual(v.disclosure.integrityStatus, 'commit-pinned');
const joined = trust.summarizeDisclosure(v.disclosure).join('\n');
assert.match(joined, /pinned to a git commit/i);
assert.doesNotMatch(joined, /sha512 pin/i, 'a commit pin must not be reported as a sha512 pin');
});
test('#3514: legacy callers (no integrityPin) see no integrity line — byte-identical rendering', () => {
const base = {
parsed: { kind: 'tarball', raw: 'https://example.com/cap.tgz', target: 'https://example.com/cap.tgz' },
manifest: EXEC_MANIFEST_3514,
hostVersion: '1.6.0',
};
const v = trust.evaluateInstallTrust(base);
assert.strictEqual(v.disclosure.integrityStatus, undefined, 'absent arg ⇒ absent field');
const joined = trust.summarizeDisclosure(v.disclosure).join('\n');
assert.doesNotMatch(joined, /PINNED|unverified/i, 'no integrity line for legacy callers');
});
test('#3514: unverified line also renders on the declarative-only disclosure path', () => {
const d = trust.discloseExecutableSurfaces({ id: 'x', skills: ['s'] });
d.integrityStatus = 'unverified';
const joined = trust.summarizeDisclosure(d).join('\n');
assert.match(joined, /NO PINNED HASH/i);
});
test('#3514: integrityStatus NEVER enters the consent signature (executableSetChanged stable)', () => {
const d1 = trust.discloseExecutableSurfaces(EXEC_MANIFEST_3514);
const d2 = trust.discloseExecutableSurfaces(EXEC_MANIFEST_3514);
d2.integrityStatus = 'unverified';
assert.strictEqual(
trust.executableSetChanged(d1, d2),
false,
'a prompt-only integrity line must not read as a changed executable set'
);
});
// ---------------------------------------------------------------------------
// #3515 (epic #1900 F20) — MCP server config is INTENTIONALLY unconfined
// (unlike hooks); the consent disclosure must say so. Adopted decision: the