Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ Publishing the GitHub release and tagging `v0.2` is a manual step after merge.
- `CITATION.cff` for GitHub’s *Cite this repository* button.
- Project-home links to [auxfirst.com](https://auxfirst.com).

## aux-audit 0.3.0

- **agent-spec v1 declares memory.** A required block, per scope: name, retention,
visibility, editability, lawful basis, plus a forget mechanism. Shape follows
`memory-policy.schema.yaml`, so nothing new was invented. `aux.H08` is scoreable
under v1 and `aux.T02` Contextual Trust is assessable again — the three memory
gaps in the taxonomy are reachable from a v1 spec.
- Memory the user cannot see costs `aux.H02`; memory they cannot correct costs
`aux.H03`. Same split v0 made, where `user_editable` fed H03.
- **The audit argues with the spec's claim.** `trust_stage` is compared against the
computed stage, never used as input. Claiming more than the evidence supports is
a `high` finding naming what blocks it. Claiming less is reported and nothing more.

## aux-audit 0.2.0

- Scores **agent-spec v1.0**. Both formats are read, detected from the document
Expand Down
8 changes: 7 additions & 1 deletion packages/aux-audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ Under **v1.0** the audit asks a better question. Instead of *"what did you call

Under **v0.1.0** nothing changed: the same label and the same prose guarantees produce the same scores as before. Scores are not comparable across the two, so `meta.spec_version` records which one produced them.

One thing v1 cannot express: **memory**. It has no field for persistence, scoping, or retention, so `aux.H08` is reported as not scoreable rather than scored zero — and `aux.T02` Contextual Trust, whose only backing heuristic that is, comes back *not assessable* rather than earned. See [trustkit#10](https://github.com/auxfirst/trustkit/issues/10).
**v1 declares memory**, per scope: what is kept, for how long, whether the user can see it, correct it, and have it forgotten. `aux.H08` scores whether the memory is bounded and forgettable; visibility feeds `aux.H02` and editability `aux.H03`, the same split v0 made.

**The audit argues with the spec's own claim.** A v1 spec may declare a `trust_stage`. That is never an input to the score — it is compared against it, and a spec claiming more than it shows gets told so:

> **The spec claims *advocacy*; the evidence supports *no stage at all*.** A stage is earned by what the spec can show, not by what it says about itself.

Under-claiming is surfaced next to the computed stage and nothing more. Modesty is not a defect.

Two consequences, both deliberate:

Expand Down
18 changes: 18 additions & 0 deletions packages/aux-audit/fixtures/v1-strong-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,21 @@ status: live
# The approver sees the draft but not the payment-history evidence behind the
# ranking. Tracked as tg.judgment.confident_nonsense risk; fix is the
# confidence-cues pattern on the approval queue.
memory:
persistent: true
scopes:
- name: invoice_history
purpose: Recognise repeat late payers without re-reading the ledger each run.
retention: P365D
user_visible: true
user_editable: true
lawful_basis: legitimate_interest
- name: contact_preferences
purpose: Respect a customer's stated channel and cadence.
retention: P90D
user_visible: true
user_editable: true
lawful_basis: consent
forget:
available: true
mechanism: Deletion request routed to the data team; purges both scopes within 30 days.
7 changes: 7 additions & 0 deletions packages/aux-audit/fixtures/v1-weak-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,10 @@ exceptions:
shutdown:
procedure: "Revoke the OAuth token in the admin console."
tested: false
memory:
persistent: true
scopes:
- name: everything_seen
retention: P3650D
user_visible: false
user_editable: false
2 changes: 1 addition & 1 deletion packages/aux-audit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aux-audit",
"version": "0.2.0",
"version": "0.3.0",
"description": "Score an agent product against the 10 AUX Heuristics and the Trust Architecture. Reproducible, CI-friendly, SARIF-capable.",
"keywords": [
"aux",
Expand Down
59 changes: 59 additions & 0 deletions packages/aux-audit/schemas/agent-spec.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ required:
- trigger
- systems
- capability
- memory
- mandate
- human_control
- exceptions
Expand Down Expand Up @@ -152,6 +153,64 @@ properties:
type: array
items: { type: string }

memory:
type: object
description: >-
What the agent retains between runs. Required: an agent with no memory
declares `persistent: false` rather than staying silent, because silence
and "none" are different claims and only one of them is checkable.
Shape follows memory-policy.schema.yaml.
additionalProperties: false
required:
- persistent
properties:
persistent:
type: boolean
description: Does anything survive the end of a run?
scopes:
type: array
description: >-
One entry per category of retained content. Required when persistent
is true — "we remember things" is not a memory policy.
items:
type: object
additionalProperties: false
required:
- name
- retention
- user_visible
- user_editable
properties:
name:
type: string
description: The category, e.g. preferences, conversation_history.
purpose:
type: string
retention:
type: string
pattern: "^P(?!$)(\\d+Y)?(\\d+M)?(\\d+W)?(\\d+D)?(T(?!$)(\\d+H)?(\\d+M)?(\\d+S)?)?$"
description: ISO-8601 duration. P90D, P1Y. "Indefinite" is a decision, so write it as one.
user_visible:
type: boolean
user_editable:
type: boolean
lawful_basis:
type: string
enum: [consent, contract, legitimate_interest, legal_obligation, not_applicable]
forget:
type: object
description: >-
Deletion on request. A retention period with no forget mechanism is a
promise about the calendar, not a control the user holds.
additionalProperties: false
required:
- available
properties:
available:
type: boolean
mechanism:
type: string

mandate:
type: array
description: >-
Expand Down
2 changes: 1 addition & 1 deletion packages/aux-audit/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { toMarkdown } from "./report/markdown.js";
import { toSarif } from "./report/sarif.js";
import { SEVERITY_ORDER, type Severity } from "./types.js";

const VERSION = "0.2.0";
const VERSION = "0.3.0";
const FORMATS = new Set(["md", "markdown", "json", "sarif"]);
const SEVERITIES = new Set(["low", "medium", "high", "critical"]);

Expand Down
25 changes: 23 additions & 2 deletions packages/aux-audit/src/evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export interface MemoryEvidence {
scopeCount: number;
userVisible: boolean;
userEditable: boolean;
/** v1 only: every scope names a retention period. v0 had one for all of them. */
everyScopeRetained?: boolean;
/** v1 only: deletion on request is a control the user holds, not a calendar. */
forgettable?: boolean;
}

export interface MandateEvidence {
Expand Down Expand Up @@ -62,6 +66,12 @@ export interface Evidence {

/** True once the spec carries evidence of testing, which gates level 3. */
evidenced: boolean;

/**
* The trust stage the spec claims for itself. v1 only, and the audit's job is
* to agree or disagree with it — not to take it as input to the score.
*/
claimedStage?: string;
}

/**
Expand Down Expand Up @@ -137,8 +147,18 @@ export function fromV1(spec: AgentSpecV1): Evidence {
escalationDefault: spec.escalation_default !== undefined,
shutdownTested: spec.shutdown.tested,
modelPinned: spec.model?.version_pinned ?? false,
// v1 declares no memory. Not false — absent. See trustkit#10.
memory: undefined,
memory: {
persistent: spec.memory.persistent,
scopeCount: (spec.memory.scopes ?? []).length,
// v1 declares visibility per scope. A memory is user-visible only if
// every retained category is — one hidden scope is a hidden scope.
userVisible: (spec.memory.scopes ?? []).every((s) => s.user_visible),
userEditable: (spec.memory.scopes ?? []).every((s) => s.user_editable),
everyScopeRetained: (spec.memory.scopes ?? []).every(
(s) => typeof s.retention === "string" && s.retention.length > 1,
),
forgettable: spec.memory.forget?.available === true,
},
guarantees: [],
tools: spec.capability.can_change,
flows: [],
Expand All @@ -148,5 +168,6 @@ export function fromV1(spec: AgentSpecV1): Evidence {
// A tested shutdown and a pinned model are v1's evidence of exercise, in
// the way transcripts are v0's.
evidenced: spec.shutdown.tested === true,
claimedStage: spec.trust_stage,
};
}
7 changes: 7 additions & 0 deletions packages/aux-audit/src/report/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ export function toMarkdown(report: AuditReport): string {
lines.push(
`**Score ${report.score}/100 · Grade ${report.grade} · Trust stage: ${report.trust_stage ?? "none earned"}**`,
);
if (report.trust_stage_claimed && report.trust_stage_claimed !== report.trust_stage) {
lines.push("");
lines.push(
`> **The spec claims *${report.trust_stage_claimed}*; the evidence supports *${report.trust_stage ?? "no stage at all"}*.** ` +
"A stage is earned by what the spec can show, not by what it says about itself.",
);
}
lines.push("");

lines.push("### Trust Architecture");
Expand Down
35 changes: 33 additions & 2 deletions packages/aux-audit/src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,12 @@ export const RULES: Rule[] = [
evaluate(e) {
if (isV1(e)) {
const observe = control(e, "observe");
const memoryHidden = e.memory !== undefined && e.memory.persistent && !e.memory.userVisible;
const level = clamp(
(has(e, "observe") ? 1 : 0) +
(supervises(e, "provenance_at_decision") ? 1 : 0) +
(observe?.audience === "affected_user" ? 1 : 0),
(observe?.audience === "affected_user" ? 1 : 0) -
(memoryHidden ? 1 : 0),
);
return {
level,
Expand All @@ -119,6 +121,7 @@ export const RULES: Rule[] = [
observe?.audience === "affected_user"
? "visible to the affected user, not only operators"
: `observability reaches ${observe?.audience ?? "no one"}`,
memoryHidden && "at least one retained scope is hidden from the user",
]),
};
}
Expand All @@ -143,10 +146,12 @@ export const RULES: Rule[] = [
id: "aux.H03",
evaluate(e) {
if (isV1(e)) {
const memoryStuck = e.memory !== undefined && e.memory.persistent && !e.memory.userEditable;
const level = clamp(
(has(e, "interrupt") ? 1 : 0) +
(has(e, "override") ? 1 : 0) +
(supervises(e, "reversal") ? 1 : 0),
(supervises(e, "reversal") ? 1 : 0) -
(memoryStuck ? 1 : 0),
);
return {
level,
Expand All @@ -155,6 +160,7 @@ export const RULES: Rule[] = [
has(e, "interrupt") ? "a run can be interrupted mid-flight" : "no interrupt control",
has(e, "override") ? "a decision can be overridden" : "no override control",
supervises(e, "reversal") ? "actions are reversible" : "actions are not reversible",
memoryStuck && "a retained scope cannot be corrected by the user",
]),
};
}
Expand Down Expand Up @@ -367,6 +373,31 @@ export const RULES: Rule[] = [
};
}
const scopes = e.memory.scopeCount;
if (isV1(e)) {
// v1 declares retention and visibility per scope, so it can say more
// than "memory exists": whether each category is bounded, and whether
// the user can make it go away.
const level = clamp(
(scopes >= 1 ? 1 : 0) +
(e.memory.everyScopeRetained ? 1 : 0) +
(e.memory.forgettable ? 1 : 0),
);
return {
level,
applicable: true,
evidence: join([
scopes >= 1
? `memory is persistent across ${scopes} scope(s)`
: "memory is persistent but no scope is declared — what is kept is unstated",
e.memory.everyScopeRetained
? "every scope names a retention period"
: "at least one scope is retained indefinitely, or does not say",
e.memory.forgettable
? "the user can have it forgotten on request"
: "no forget mechanism — retention is a promise about the calendar, not a control",
]),
};
}
return {
level: clamp(2 + (scopes >= 2 ? 1 : 0)),
applicable: true,
Expand Down
30 changes: 29 additions & 1 deletion packages/aux-audit/src/score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ export function audit(evidence: Evidence, options: AuditOptions = {}): AuditRepo
const highest = [...trust_stages].reverse().find((s) => s.earned);
const trust_stage = highest ? highest.name.toLowerCase().replace(/ trust$/, "") : null;

// A spec may claim a stage. The audit's job is to agree or disagree with the
// claim, never to take it as input — so it is compared, not scored.
const claimed = evidence.claimedStage
? trust_stages.find((s) => s.id === evidence.claimedStage)
: undefined;
const trust_stage_claimed = claimed
? claimed.name.toLowerCase().replace(/ trust$/, "")
: null;
const earnedOrder = highest?.order ?? 0;

const issues: Issue[] = [];
const recommendations: string[] = [];

Expand Down Expand Up @@ -195,6 +205,23 @@ export function audit(evidence: Evidence, options: AuditOptions = {}): AuditRepo
});
}

if (claimed && claimed.order > earnedOrder) {
const blocker = trust_stages
.filter((s) => s.order <= claimed.order && !s.earned)
.flatMap((s) =>
s.assessable ? s.shortfall : [`${s.depends_on.join(", ")} (not assessable)`],
);
issues.push({
id: claimed.id,
type: "overclaimed_trust_stage",
severity: "high",
evidence:
`the spec claims ${claimed.id} ${claimed.name}; the evidence supports ` +
`${highest ? `${highest.id} ${highest.name}` : "no stage at all"}` +
(blocker.length > 0 ? ` — blocked by ${[...new Set(blocker)].join(", ")}` : ""),
});
}

issues.sort(
(a, b) =>
SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity] ||
Expand All @@ -206,6 +233,7 @@ export function audit(evidence: Evidence, options: AuditOptions = {}): AuditRepo
score,
grade: grade(score),
trust_stage,
trust_stage_claimed,
evolution_stage: null,
evolution_stage_status: "human-assessed",
heuristics,
Expand All @@ -215,7 +243,7 @@ export function audit(evidence: Evidence, options: AuditOptions = {}): AuditRepo
meta: {
tool: "aux-audit",
spec_version: evidence.specVersion,
tool_version: options.toolVersion ?? "0.2.0",
tool_version: options.toolVersion ?? "0.3.0",
heuristics_version: canon.heuristicsVersion,
trust_architecture_version: canon.trustArchitectureVersion,
taxonomy_version: canon.taxonomyVersion,
Expand Down
16 changes: 16 additions & 0 deletions packages/aux-audit/src/spec-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ export interface ExceptionRow {
escalation?: Escalation;
}

export interface MemoryScope {
name: string;
purpose?: string;
retention: string;
user_visible: boolean;
user_editable: boolean;
lawful_basis?: string;
}

export interface MemorySpecV1 {
persistent: boolean;
scopes?: MemoryScope[];
forget?: { available: boolean; mechanism?: string };
}

export interface AgentSpecV1 {
spec_version: string;
id: string;
Expand All @@ -64,6 +79,7 @@ export interface AgentSpecV1 {
users?: unknown[];
systems: { data_sources: string[]; connected: { name: string; auth: unknown }[] };
capability: { can_read: string[]; can_change: string[] };
memory: MemorySpecV1;
mandate: MandateRow[];
human_control: Record<"observe" | "interrupt" | "approve" | "override" | "disable", Control>;
exceptions: ExceptionRow[];
Expand Down
5 changes: 5 additions & 0 deletions packages/aux-audit/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export interface AuditReport {
score: number;
grade: Grade;
trust_stage: string | null;
/**
* The stage the spec claimed, when it claimed one. Reported next to the
* computed stage so the two can disagree in public.
*/
trust_stage_claimed: string | null;
/**
* Always null, by design — not pending.
*
Expand Down
Loading
Loading