@maintainabilityai/research-runner 0.1.41 → 0.1.43

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.
@@ -1881,6 +1881,12 @@ const handleAuditVerifyChain = async (input) => {
1881
1881
  // Track signature state across the whole chain.
1882
1882
  let signedCount = 0;
1883
1883
  let workflowUnsignedCount = 0; // post-agent workflow-emitted events, unsigned by-design
1884
+ // P9 (Bug-P / Codex audit): revise-agent unsigned events get their own
1885
+ // bucket so we can decide legitimacy chain-by-chain — legacy chains
1886
+ // (no per-epoch signing anywhere) keep the old allowance; per-epoch
1887
+ // chains (any event with `signer_epoch`) require revise-agent to sign.
1888
+ let reviseAgentUnsignedCount = 0;
1889
+ let chainUsesPerEpochSigning = false;
1884
1890
  let prev = null;
1885
1891
  for (let i = 0; i < lines.length; i++) {
1886
1892
  let event;
@@ -1911,23 +1917,52 @@ const handleAuditVerifyChain = async (input) => {
1911
1917
  if (recordedHash !== recomputed) {
1912
1918
  return { ok: false, reason: `forged-hash-line-${i + 1}: recorded=${recordedHash.slice(0, 16)}… recomputed=${recomputed.slice(0, 16)}…` };
1913
1919
  }
1914
- // Bug K + N (cert-run-5): post-agent events (payload.emitted_by in
1915
- // {'workflow', 'revise-agent'}) carry signature: '' by design the
1916
- // post-agent context can't sign because the ephemeral private key is
1917
- // gone. Count them separately so they don't trip partial-tampering
1918
- // detection or signature verification. Both attributions are
1919
- // legitimate-unsigned.
1920
+ // Bug K + N (cert-run-5): post-agent events emitted by the workflow
1921
+ // (e.g. the synthetic self_review backfill that runs AFTER the agent
1922
+ // session ended) genuinely cannot sign the ephemeral private key
1923
+ // is gone by then. `payload.emitted_by: 'workflow'` is the legitimate
1924
+ // unsigned attribution.
1925
+ //
1926
+ // P9 (Bug-P / Codex audit) — `revise-agent` used to share the same
1927
+ // legitimate-unsigned bucket because Bug N landed BEFORE Bug O. With
1928
+ // per-epoch signing (Bug O), a revise-agent session DOES have an
1929
+ // ephemeral key and DOES sign its events. So an unsigned revise-agent
1930
+ // event is now only legitimate on LEGACY chains — chains where no
1931
+ // event carries `signer_epoch`. Tracking that requires a chain-level
1932
+ // verdict, so we count unsigned revise-agent events into a separate
1933
+ // bucket and decide legitimacy after the loop sees whether the chain
1934
+ // uses per-epoch signing at all.
1920
1935
  const eventPayload = event.payload;
1921
1936
  const emittedBy = eventPayload?.emitted_by;
1922
- const isUnsignedByDesign = emittedBy === 'workflow' || emittedBy === 'revise-agent';
1937
+ const isWorkflowUnsigned = emittedBy === 'workflow';
1938
+ const isReviseAgentUnsigned = emittedBy === 'revise-agent';
1939
+ if (typeof event.signer_epoch === 'number') {
1940
+ chainUsesPerEpochSigning = true;
1941
+ }
1923
1942
  if (recordedSignature !== null && recordedSignature !== '') {
1924
1943
  signedCount++;
1925
1944
  }
1926
- else if (isUnsignedByDesign) {
1945
+ else if (isWorkflowUnsigned) {
1927
1946
  workflowUnsignedCount++;
1928
1947
  }
1948
+ else if (isReviseAgentUnsigned) {
1949
+ reviseAgentUnsignedCount++;
1950
+ }
1929
1951
  prev = recordedHash;
1930
1952
  }
1953
+ // P9: legacy chains (pre-Bug-O — no signer_epoch on any event) keep
1954
+ // the broad allowance. New chains (any event carries signer_epoch)
1955
+ // require revise-agent events to be signed; an unsigned revise-agent
1956
+ // event on a per-epoch chain is now a real chain-integrity failure.
1957
+ if (chainUsesPerEpochSigning && reviseAgentUnsignedCount > 0) {
1958
+ return {
1959
+ ok: false,
1960
+ reason: `revise-agent-unsigned-on-per-epoch-chain: ${reviseAgentUnsignedCount} revise-agent events without signatures; per-epoch chains require revise-agent to sign with its own epoch key (Bug O contract)`,
1961
+ };
1962
+ }
1963
+ // Legacy chains: roll revise-agent unsigned into the workflow-unsigned
1964
+ // bucket so the downstream "agent_event_count" math still excludes them.
1965
+ workflowUnsignedCount += reviseAgentUnsignedCount;
1931
1966
  // Knight's Seal verification: every AGENT-emitted event must be signed
1932
1967
  // by its declared signer_epoch's pub key. Workflow-unsigned events are
1933
1968
  // excluded from the denominator (their emitted_by: 'workflow' marker
@@ -1935,6 +1970,24 @@ const handleAuditVerifyChain = async (input) => {
1935
1970
  const sealed = signedCount > 0;
1936
1971
  const agentEventCount = lines.length - workflowUnsignedCount;
1937
1972
  let sealVerified = false;
1973
+ // Bug-Q / Q3 (Codex audit round 2) — a chain that USES per-epoch
1974
+ // signing (any event carries `signer_epoch`) MUST be sealed AND seal-
1975
+ // verified. Without this guard, an attacker could hand-craft a chain
1976
+ // where event 1 is signed (forcing `chainUsesPerEpochSigning=true`)
1977
+ // but every subsequent event is unsigned — `signedCount > 0` would
1978
+ // be true and the per-event check below would pass each unsigned
1979
+ // event as `legitimateUnsigned` if attribution were faked. Equally,
1980
+ // a chain where the runner reports `sealed=true` but the legacy
1981
+ // `chainUsesPerEpochSigning=false` path runs is the gold-product
1982
+ // promise we make to the marketing page. Legacy chains (no event
1983
+ // carries signer_epoch) keep the prior allowance — they predate
1984
+ // Bug O and a user audit-replaying them is intentionally tolerant.
1985
+ if (chainUsesPerEpochSigning && !sealed) {
1986
+ return {
1987
+ ok: false,
1988
+ reason: `per-epoch-chain-not-sealed: chain references signer_epoch (per-epoch signing contract) but no events carry signatures; gold-product contract requires per-epoch chains to be fully sealed`,
1989
+ };
1990
+ }
1938
1991
  if (sealed) {
1939
1992
  if (signedCount !== agentEventCount) {
1940
1993
  return { ok: false, reason: `partial-signatures: ${signedCount}/${agentEventCount} agent-emitted events signed (chain tampered; ${workflowUnsignedCount} workflow-emitted unsigned by-design)` };
@@ -1945,9 +1998,14 @@ const handleAuditVerifyChain = async (input) => {
1945
1998
  for (let i = 0; i < lines.length; i++) {
1946
1999
  const event = JSON.parse(lines[i]);
1947
2000
  const emittedBy = event.payload?.emitted_by;
1948
- const isLegacyUnsigned = (emittedBy === 'workflow' || emittedBy === 'revise-agent')
2001
+ // P9: workflow-emitted unsigned events are always legitimate
2002
+ // (the post-agent context genuinely has no private key). For
2003
+ // revise-agent unsigned events, the loop above already returned
2004
+ // an error if we're on a per-epoch chain, so reaching this point
2005
+ // means we're on a legacy chain where the looser bucket applies.
2006
+ const isLegitimateUnsigned = (emittedBy === 'workflow' || emittedBy === 'revise-agent')
1949
2007
  && (!event.signature || event.signature === '');
1950
- if (isLegacyUnsigned) {
2008
+ if (isLegitimateUnsigned) {
1951
2009
  continue;
1952
2010
  }
1953
2011
  // Bug O (Task #72) — per-epoch verification. Events default to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainabilityai/research-runner",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "Research + PRD agent runner — orchestrates the Archeologist and PRD pipelines for the MaintainabilityAI governance mesh",
5
5
  "license": "MIT",
6
6
  "author": "MaintainabilityAI",