diff --git a/node_modules b/node_modules new file mode 120000 index 00000000..4c37106c --- /dev/null +++ b/node_modules @@ -0,0 +1 @@ +/home/ruvultra/projects/agent-harness-generator/node_modules \ No newline at end of file diff --git a/packages/create-agent-harness/__tests__/publish.test.ts b/packages/create-agent-harness/__tests__/publish.test.ts index 3e1e13e0..591b6b74 100644 --- a/packages/create-agent-harness/__tests__/publish.test.ts +++ b/packages/create-agent-harness/__tests__/publish.test.ts @@ -34,6 +34,39 @@ describe('publishHarness (dry-run path)', () => { }); }); +// GH #4 (HIGH-1): a present witness whose signature was NOT cryptographically verified (degraded mode — +// no kernel verifier) must NOT publish silently as "signed". Before the fix the shape-only path returned +// valid:true, so a shape-valid witness carrying a garbage signature published as verified. +describe('publishHarness — fail-closed on an unverified witness (GH #4 HIGH-1)', () => { + async function harnessWithGarbageWitness(): Promise { + const root = await mkdtemp(join(tmpdir(), 'cah-pub-wit-')); + await mkdir(join(root, '.harness'), { recursive: true }); + await writeFile(join(root, '.harness', 'manifest.json'), + JSON.stringify({ schema: 1, template: 'minimal', vars: { name: 'demo' } })); + // Shape-valid witness with a GARBAGE signature — the exploit input. + await writeFile(join(root, '.harness', 'witness.json'), JSON.stringify({ + schema: 1, harness: 'demo', version: '0.1.0', entries: [], + public_key: 'a'.repeat(64), signature: 'b'.repeat(128), + })); + return root; + } + + it('refuses to publish a harness whose witness signature was never checked', async () => { + const root = await harnessWithGarbageWitness(); + await expect(publishHarness({ + harnessDir: root, pinata: { jwt: 'x' }, confirm: false, + })).rejects.toThrow(/NOT cryptographically verified|--allow-unverified-witness/); + }); + + it('publishes when --allow-unverified-witness is explicitly opted in', async () => { + const root = await harnessWithGarbageWitness(); + const r = await publishHarness({ + harnessDir: root, pinata: { jwt: 'x' }, confirm: false, allowUnverified: true, + }); + expect(r.manifestCid).toBe('dry-run-no-pin'); + }); +}); + describe('pinJson', () => { it('throws on missing JWT', async () => { await expect(pinJson({ jwt: '' }, { foo: 'bar' }, { name: 't' })) diff --git a/packages/create-agent-harness/src/publish-cmd.ts b/packages/create-agent-harness/src/publish-cmd.ts index 7811555e..a20b4960 100644 --- a/packages/create-agent-harness/src/publish-cmd.ts +++ b/packages/create-agent-harness/src/publish-cmd.ts @@ -23,9 +23,13 @@ export async function publishCmd(args: string[]): Promise { const positional = args.filter(a => !a.startsWith('--')); const dir = resolve(positional[0] ?? process.cwd()); const confirm = args.includes('--confirm'); + const allowUnverified = args.includes('--allow-unverified-witness'); const nameOverride = args.find(a => a.startsWith('--name='))?.slice('--name='.length); const lines: string[] = [`harness publish — ${dir} ${confirm ? '(CONFIRMED)' : '(DRY-RUN)'}`]; + if (allowUnverified) { + lines.push(' ⚠️ --allow-unverified-witness: the witness signature will NOT be cryptographically checked.'); + } if (confirm) { const jwt = process.env.PINATA_JWT; @@ -47,6 +51,7 @@ export async function publishCmd(args: string[]): Promise { }, confirm, name: nameOverride, + allowUnverified, }); lines.push(` manifest CID: ${r.manifestCid}`); lines.push(` size: ${r.manifestSize} bytes`); diff --git a/packages/create-agent-harness/src/publish.ts b/packages/create-agent-harness/src/publish.ts index 5e9d9140..276df55c 100644 --- a/packages/create-agent-harness/src/publish.ts +++ b/packages/create-agent-harness/src/publish.ts @@ -89,6 +89,12 @@ export interface HarnessPublishOptions { confirm: boolean; /** Optional override of the harness's name (defaults to manifest.vars.name). */ name?: string; + /** + * GH #4 (HIGH-1): explicitly publish even when a present witness could NOT be cryptographically + * verified (no verifier available). Off by default — publishing is fail-closed on an unverified + * witness so a garbage signature can't masquerade as signed. The signature is NOT checked when set. + */ + allowUnverified?: boolean; } export interface PublishResult { @@ -128,6 +134,17 @@ export async function publishHarness(opts: HarnessPublishOptions): Promise