Skip to content

Commit 2aeb2fe

Browse files
committed
Adjudicate consent before the fetch, not after the bytes land
resolve_steering_artifacts loaded a sourceset, checked --allow-network, and downloaded. The sourceset's policy block has always declared requiresGrant, requiresPolicyAdmission, requiresStorageReceipt and requiresEvidence, and the resolver read none of them; the receipt itself admitted the sequence, noting that a grant and a policy admission were still required before applied steering could be "accepted" — accepted, not fetched. The ordering is the guarantee. A grant adjudicated after arrival governs whether artifacts may be USED; it cannot govern whether they may ARRIVE, and by the time it is consulted the disk, the bandwidth and the exposure have already been spent. --allow-network is an operator affordance meaning this box has connectivity. It has never meant a person agreed to these bytes landing on this disk, and no schema in the repo had a field where that agreement could be recorded. ArtifactConsentRecord is that field. It carries who agreed and how they were asked (an interactive operator and a fleet default are different evidence and should not be indistinguishable afterwards), and what they were actually shown — the footprint and every remote that will be contacted. A repo absent from the disclosure was not disclosed, so a fetch that would reach it is refused. Three distinctions the schema deliberately preserves. Scope: agreeing something may run if present is not agreeing it may be put there, so an activation-only record does not authorise a download. Decision: a declined record is retained, because a refusal and a never-asked are different states and only a retained refusal can stop a silent re-prompt. Revocation: recorded on the decision rather than by deleting it, so a withdrawal leaves a trace instead of resembling a decision never made. The declared-but-unread policy block is now named in the receipt as outstandingPolicyRequirements. Resolution genuinely cannot discharge those — they gate activation — but it can stop presenting an unread policy block as a satisfied one. validate-consent-before-staging carries eight refusals, because the property is a refusal and a gate never observed refusing is indistinguishable from no gate. The last check drives the real entry point with --allow-network and no consent, with a tripwire on the fetch path: it fails if execution ever reaches the point where a remote would be contacted. Removing the gate makes that check fail, which is how I know it is load-bearing rather than decorative. The merge-duplication detector rejected my first Makefile edit for adding a second .PHONY line. It was right; the targets are collapsed by union instead.
1 parent 4474b46 commit 2aeb2fe

13 files changed

Lines changed: 605 additions & 7 deletions

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: validate-no-merge-duplication validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-agent-registry validate-superconscious-runtime-plan validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-runtime-install-receipts doctor probe validate-artifact-digest-honesty
1+
.PHONY: validate-no-merge-duplication validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-agent-registry validate-superconscious-runtime-plan validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-runtime-install-receipts doctor probe validate-artifact-digest-honesty validate-consent-before-staging
22

33
PYTHON ?= python3
44
RUBY ?= ruby
@@ -22,7 +22,7 @@ DECIDED_AT := 2026-05-04T12:51:00Z
2222
PYCLI := PYTHONPATH=src $(PYTHON) -m agent_machine.cli
2323
PYMOD := PYTHONPATH=src $(PYTHON) -m
2424

25-
validate: validate-no-merge-duplication validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-agent-registry validate-superconscious-runtime-plan validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-runtime-install-receipts validate-artifact-digest-honesty
25+
validate: validate-no-merge-duplication validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-agent-registry validate-superconscious-runtime-plan validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-runtime-install-receipts validate-artifact-digest-honesty validate-consent-before-staging
2626

2727
validate-no-merge-duplication:
2828
$(PYTHON) scripts/validate-no-merge-duplication.py
@@ -31,6 +31,9 @@ validate-no-merge-duplication:
3131
validate-artifact-digest-honesty:
3232
$(PYTHON) scripts/validate-artifact-digest-honesty.py
3333

34+
validate-consent-before-staging:
35+
$(PYTHON) scripts/validate-consent-before-staging.py
36+
3437
validate-json:
3538
$(PYTHON) scripts/validate-json.py
3639

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "urn:srcos:agent-machine:schema:artifact-consent-record:v0.1.0",
4+
"title": "ArtifactConsentRecord",
5+
"description": "Evidence that a user agreed to specific artifacts landing on their disk, recorded BEFORE those bytes are fetched. This exists because the ordering is the guarantee: a grant adjudicated after download governs whether the artifacts may be USED, not whether they may ARRIVE, and the two are not the same promise. A machine that pre-stages regardless of opt-in and adjudicates afterwards has already spent the disk, the bandwidth and the exposure it was asking permission for.",
6+
"type": "object",
7+
"additionalProperties": false,
8+
"required": ["specVersion", "id", "kind", "sourcesetId", "scope", "decision", "decidedAt", "subject", "disclosure"],
9+
"properties": {
10+
"specVersion": { "type": "string", "const": "0.1.0" },
11+
"id": {
12+
"type": "string",
13+
"pattern": "^urn:srcos:agent-machine:artifact-consent-record:[a-z0-9][a-z0-9.-]*$"
14+
},
15+
"kind": { "type": "string", "const": "ArtifactConsentRecord" },
16+
"sourcesetId": {
17+
"type": "string",
18+
"pattern": "^[a-z0-9][a-z0-9.-]*$",
19+
"description": "The sourceset this decision covers. Consent is per-sourceset; it does not generalise to others."
20+
},
21+
"scope": {
22+
"type": "string",
23+
"enum": ["download", "activation", "download-and-activation"],
24+
"description": "What was agreed to. 'activation' alone does NOT authorise a fetch: agreeing that something may run if present is not agreeing that it may be placed there. A resolver must refuse to download under an activation-only record."
25+
},
26+
"decision": {
27+
"type": "string",
28+
"enum": ["granted", "declined"],
29+
"description": "A declined record is retained deliberately. Absence of consent and refusal of consent are different states, and silently re-prompting past a refusal is how an opt-out becomes advisory."
30+
},
31+
"decidedAt": { "type": "string", "format": "date-time" },
32+
"expiresAt": {
33+
"type": ["string", "null"],
34+
"format": "date-time",
35+
"description": "After this instant the record no longer authorises a fetch. Null means it does not lapse on its own."
36+
},
37+
"revokedAt": {
38+
"type": ["string", "null"],
39+
"format": "date-time",
40+
"description": "Revocation is recorded on the decision itself rather than by deleting it, so that a withdrawal leaves a trace instead of resembling a decision never made."
41+
},
42+
"subject": {
43+
"type": "object",
44+
"additionalProperties": false,
45+
"required": ["principalRef", "attestation"],
46+
"description": "WHO agreed. An operator flag such as --allow-network says a machine has connectivity; it does not say a person consented, and the two must not be conflated.",
47+
"properties": {
48+
"principalRef": { "type": "string", "minLength": 1 },
49+
"attestation": {
50+
"type": "string",
51+
"enum": ["interactive-operator", "delegated-policy", "automated-fleet"],
52+
"description": "How the agreement was obtained. Recorded because a fleet default and a person clicking accept are different evidence and should not be indistinguishable after the fact."
53+
}
54+
}
55+
},
56+
"disclosure": {
57+
"type": "object",
58+
"additionalProperties": false,
59+
"required": ["declaredBytes", "purpose", "artifactRepos"],
60+
"description": "What the subject was actually told. Consent to an undisclosed quantity is not consent to any quantity, so the resolver checks what was disclosed against what it is about to fetch.",
61+
"properties": {
62+
"declaredBytes": {
63+
"type": "integer",
64+
"minimum": 0,
65+
"description": "Upper bound of on-disk footprint disclosed at the time of asking."
66+
},
67+
"purpose": { "type": "string", "minLength": 1 },
68+
"artifactRepos": {
69+
"type": "array",
70+
"minItems": 1,
71+
"items": { "type": "string" },
72+
"description": "Every remote the fetch will contact. A repo absent here was not disclosed and must not be fetched under this record."
73+
},
74+
"retention": {
75+
"type": "string",
76+
"enum": ["until-revoked", "session", "single-use"],
77+
"description": "How long the artifacts may remain after the fetch."
78+
}
79+
}
80+
},
81+
"notes": { "type": "array", "items": { "type": "string" } }
82+
}
83+
}

contracts/steering-artifact-receipt.schema.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,71 @@
2828
"includeAuthMaterial": { "type": "boolean", "const": false }
2929
}
3030
},
31+
"consent": {
32+
"type": "object",
33+
"additionalProperties": false,
34+
"description": "The consent decision as adjudicated, and CRUCIALLY when. checkedBefore='download' asserts no byte was requested until the decision passed; a receipt that cannot make that claim should not imply it.",
35+
"required": [
36+
"required",
37+
"checkedBefore"
38+
],
39+
"properties": {
40+
"required": {
41+
"type": "boolean",
42+
"description": "Whether the sourceset demanded consent at all."
43+
},
44+
"checkedBefore": {
45+
"type": "string",
46+
"enum": [
47+
"download",
48+
"activation"
49+
],
50+
"description": "The point in the sequence at which consent was adjudicated. 'activation' means bytes had already landed."
51+
},
52+
"consentRef": {
53+
"type": [
54+
"string",
55+
"null"
56+
]
57+
},
58+
"scope": {
59+
"type": "string",
60+
"enum": [
61+
"download",
62+
"activation",
63+
"download-and-activation"
64+
]
65+
},
66+
"decidedAt": {
67+
"type": "string"
68+
},
69+
"subjectRef": {
70+
"type": "string"
71+
},
72+
"attestation": {
73+
"type": "string",
74+
"enum": [
75+
"interactive-operator",
76+
"delegated-policy",
77+
"automated-fleet"
78+
]
79+
},
80+
"declaredBytes": {
81+
"type": [
82+
"integer",
83+
"null"
84+
],
85+
"minimum": 0
86+
}
87+
}
88+
},
89+
"outstandingPolicyRequirements": {
90+
"type": "array",
91+
"items": {
92+
"type": "string"
93+
},
94+
"description": "Requirements the sourceset declares that resolution does NOT discharge. They gate activation, not fetching, so resolution cannot satisfy them \u2014 but naming them keeps a declared-and-unread policy block from reading as a satisfied one."
95+
},
3196
"notes": { "type": "array", "items": { "type": "string" } }
3297
},
3398
"$defs": {

contracts/steering-sourceset.schema.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,27 @@
127127
}
128128
}
129129
},
130+
"consent": {
131+
"type": "object",
132+
"additionalProperties": false,
133+
"description": "Whether a user must agree BEFORE these artifacts are fetched. Distinct from the policy block below, which gates activation: policy answers 'may this run', consent answers 'may this arrive'. Adjudicating only the former means the bytes, the bandwidth and the disk are already spent by the time anyone is asked.",
134+
"required": ["requiresUserConsent"],
135+
"properties": {
136+
"requiresUserConsent": {
137+
"type": "boolean",
138+
"description": "When true, resolution refuses to contact any remote without a granted, unexpired, unrevoked ArtifactConsentRecord whose scope authorises a download."
139+
},
140+
"purpose": {
141+
"type": "string",
142+
"description": "What the artifacts are for, in terms the subject was actually shown."
143+
},
144+
"estimatedBytes": {
145+
"type": "integer",
146+
"minimum": 0,
147+
"description": "Expected on-disk footprint, so the subject is asked about a quantity rather than an abstraction."
148+
}
149+
}
150+
},
130151
"policy": {
131152
"type": "object",
132153
"additionalProperties": false,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"specVersion": "0.1.0",
3+
"kind": "ArtifactConsentRecord",
4+
"sourcesetId": "gpt2-small.res-jb",
5+
"decidedAt": "2026-07-29T10:00:00Z",
6+
"expiresAt": null,
7+
"revokedAt": null,
8+
"subject": {
9+
"principalRef": "urn:srcos:principal:local-operator",
10+
"attestation": "interactive-operator"
11+
},
12+
"disclosure": {
13+
"declaredBytes": 600000000,
14+
"purpose": "Local SAE steering research on GPT-2 Small.",
15+
"artifactRepos": [
16+
"openai-community/gpt2",
17+
"jbloom/GPT2-Small-SAEs-Reformatted"
18+
],
19+
"retention": "until-revoked"
20+
},
21+
"id": "urn:srcos:agent-machine:artifact-consent-record:gpt2-activation-only",
22+
"scope": "activation",
23+
"decision": "granted",
24+
"notes": [
25+
"Agreeing that something may run if present is not agreeing that it may be placed there; this record must not authorise a fetch."
26+
]
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"specVersion": "0.1.0",
3+
"kind": "ArtifactConsentRecord",
4+
"sourcesetId": "gpt2-small.res-jb",
5+
"decidedAt": "2026-07-29T10:00:00Z",
6+
"expiresAt": null,
7+
"revokedAt": null,
8+
"subject": {
9+
"principalRef": "urn:srcos:principal:local-operator",
10+
"attestation": "interactive-operator"
11+
},
12+
"disclosure": {
13+
"declaredBytes": 600000000,
14+
"purpose": "Local SAE steering research on GPT-2 Small.",
15+
"artifactRepos": [
16+
"openai-community/gpt2",
17+
"jbloom/GPT2-Small-SAEs-Reformatted"
18+
],
19+
"retention": "until-revoked"
20+
},
21+
"id": "urn:srcos:agent-machine:artifact-consent-record:gpt2-declined",
22+
"scope": "download-and-activation",
23+
"decision": "declined",
24+
"notes": [
25+
"Retained deliberately: a refusal and a never-asked are different states, and only a retained refusal can stop a silent re-prompt."
26+
]
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"specVersion": "0.1.0",
3+
"kind": "ArtifactConsentRecord",
4+
"sourcesetId": "gpt2-small.res-jb",
5+
"decidedAt": "2026-07-29T10:00:00Z",
6+
"expiresAt": null,
7+
"revokedAt": null,
8+
"subject": {
9+
"principalRef": "urn:srcos:principal:local-operator",
10+
"attestation": "interactive-operator"
11+
},
12+
"disclosure": {
13+
"declaredBytes": 600000000,
14+
"purpose": "Local SAE steering research on GPT-2 Small.",
15+
"artifactRepos": [
16+
"openai-community/gpt2",
17+
"jbloom/GPT2-Small-SAEs-Reformatted"
18+
],
19+
"retention": "until-revoked"
20+
},
21+
"id": "urn:srcos:agent-machine:artifact-consent-record:gpt2-granted",
22+
"scope": "download-and-activation",
23+
"decision": "granted",
24+
"notes": [
25+
"The subject was shown the footprint and both remotes before any byte was requested."
26+
]
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"specVersion": "0.1.0",
3+
"kind": "ArtifactConsentRecord",
4+
"sourcesetId": "gpt2-small.res-jb",
5+
"decidedAt": "2026-07-29T10:00:00Z",
6+
"expiresAt": null,
7+
"revokedAt": "2026-07-29T12:00:00Z",
8+
"subject": {
9+
"principalRef": "urn:srcos:principal:local-operator",
10+
"attestation": "interactive-operator"
11+
},
12+
"disclosure": {
13+
"declaredBytes": 600000000,
14+
"purpose": "Local SAE steering research on GPT-2 Small.",
15+
"artifactRepos": [
16+
"openai-community/gpt2",
17+
"jbloom/GPT2-Small-SAEs-Reformatted"
18+
],
19+
"retention": "until-revoked"
20+
},
21+
"id": "urn:srcos:agent-machine:artifact-consent-record:gpt2-revoked",
22+
"scope": "download-and-activation",
23+
"decision": "granted",
24+
"notes": [
25+
"Revocation is recorded on the decision rather than by deleting it, so withdrawal leaves a trace."
26+
]
27+
}

examples/steering-sourcesets/gpt2-small-res-jb.steering-sourceset.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@
9090
"notes": [
9191
"This record does not download model weights or SAE artifacts.",
9292
"This record does not claim the sourceset is loadable today.",
93-
"This record exists so Issue #34 has a typed input target for controlled activation work."
94-
]
93+
"This record exists so Issue #34 has a typed input target for controlled activation work.",
94+
"Artifacts are not fetched without a granted ArtifactConsentRecord: consent is adjudicated before the download, not before activation."
95+
],
96+
"consent": {
97+
"requiresUserConsent": true,
98+
"purpose": "Local SAE steering research on GPT-2 Small; model weights and SAE artifacts are stored on this machine.",
99+
"estimatedBytes": 600000000
100+
}
95101
}

0 commit comments

Comments
 (0)