Skip to content

Commit 48904d4

Browse files
authored
Add model carry authorization boundary (#12)
Add ModelCarryAuthorizationBoundary schema, fixtures, validator, docs, and Makefile validation so carry profiles cannot imply prompt egress, network, tool use, download, training, promotion, or lifecycle authorization.
1 parent 054f706 commit 48904d4

7 files changed

Lines changed: 305 additions & 2 deletions

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build test validate validate-portable-ai dist release-dry-run clean
1+
.PHONY: build test validate validate-portable-ai validate-model-carry-boundary dist release-dry-run clean
22

33
BIN := sourceos-ai
44
DIST_DIR := dist
@@ -20,7 +20,10 @@ test:
2020
validate-portable-ai:
2121
python3 tools/validate_portable_ai_packs.py
2222

23-
validate: build validate-portable-ai
23+
validate-model-carry-boundary:
24+
python3 tools/validate_model_carry_authorization_boundaries.py
25+
26+
validate: build validate-portable-ai validate-model-carry-boundary
2427
python3 tools/validate_carry_refs.py
2528
bin/$(BIN) carry validate --refs examples
2629
bin/$(BIN) validate --refs examples
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://schemas.srcos.ai/model-carry/model-carry-authorization-boundary.schema.json",
4+
"title": "SourceOS Model Carry Authorization Boundary",
5+
"description": "Decision-only boundary proving a carry profile is a reference/profile object, not authorization for runtime execution, prompt egress, tool use, model download, training, promotion, or lifecycle mutation.",
6+
"type": "object",
7+
"additionalProperties": false,
8+
"required": [
9+
"schemaVersion",
10+
"kind",
11+
"boundaryId",
12+
"profileRef",
13+
"profileKind",
14+
"carryScope",
15+
"authorizationBoundary",
16+
"decisionRefs",
17+
"evidenceRefs"
18+
],
19+
"properties": {
20+
"schemaVersion": { "const": "v0.1" },
21+
"kind": { "const": "ModelCarryAuthorizationBoundary" },
22+
"boundaryId": { "type": "string", "pattern": "^urn:srcos:model-carry-boundary:" },
23+
"profileRef": { "type": "string", "pattern": "^urn:srcos:model-profile:" },
24+
"profileKind": { "const": "LocalModelProfile" },
25+
"carryScope": {
26+
"type": "object",
27+
"additionalProperties": false,
28+
"required": ["mayCarryProfile", "mayCarryServiceRef", "mayEmitEvidence", "mayRouteLocally"],
29+
"properties": {
30+
"mayCarryProfile": { "const": true },
31+
"mayCarryServiceRef": { "const": true },
32+
"mayEmitEvidence": { "const": true },
33+
"mayRouteLocally": { "type": "boolean" }
34+
}
35+
},
36+
"authorizationBoundary": {
37+
"type": "object",
38+
"additionalProperties": false,
39+
"required": [
40+
"authorizesPromptEgress",
41+
"authorizesNetworkAccess",
42+
"authorizesToolUse",
43+
"authorizesModelDownload",
44+
"authorizesTrainingOnUserData",
45+
"authorizesModelPromotion",
46+
"authorizesLifecycleMutation"
47+
],
48+
"properties": {
49+
"authorizesPromptEgress": { "const": false },
50+
"authorizesNetworkAccess": { "const": false },
51+
"authorizesToolUse": { "const": false },
52+
"authorizesModelDownload": { "const": false },
53+
"authorizesTrainingOnUserData": { "const": false },
54+
"authorizesModelPromotion": { "const": false },
55+
"authorizesLifecycleMutation": { "const": false }
56+
}
57+
},
58+
"decisionRefs": {
59+
"type": "object",
60+
"additionalProperties": false,
61+
"required": ["policyDecisionRefs", "routerDecisionRefs", "governanceLedgerRefs", "explicitPullRefs"],
62+
"properties": {
63+
"policyDecisionRefs": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
64+
"routerDecisionRefs": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
65+
"governanceLedgerRefs": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
66+
"explicitPullRefs": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
67+
}
68+
},
69+
"evidenceRefs": { "type": "array", "minItems": 1, "items": { "type": "string" }, "uniqueItems": true },
70+
"notes": { "type": "string" }
71+
}
72+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Model Carry Authorization Boundary
2+
3+
## Purpose
4+
5+
`ModelCarryAuthorizationBoundary` proves that a SourceOS local model profile is a carry-layer reference object, not an authorization object.
6+
7+
The model carry repo may carry profiles, service refs, launch hints, cache policy, and evidence expectations. It does not authorize prompt egress, network access, tool use, model download, training on user data, model promotion, or model lifecycle mutation.
8+
9+
## Boundary chain
10+
11+
```text
12+
local model profile = carry/reference object
13+
model router = routing decision
14+
policy fabric = prompt egress / network / tool-use admission
15+
model governance ledger = lifecycle, tuning, promotion, consent, revocation evidence
16+
explicit pull/install = separate operator action
17+
```
18+
19+
## Required false authorizations
20+
21+
A valid boundary record must set all of these to `false`:
22+
23+
```text
24+
authorizesPromptEgress
25+
authorizesNetworkAccess
26+
authorizesToolUse
27+
authorizesModelDownload
28+
authorizesTrainingOnUserData
29+
authorizesModelPromotion
30+
authorizesLifecycleMutation
31+
```
32+
33+
## Validation
34+
35+
```bash
36+
python3 tools/validate_model_carry_authorization_boundaries.py
37+
```
38+
39+
The validator checks one valid boundary fixture and negative fixtures for prompt egress and automatic model download.
40+
41+
## Non-goals
42+
43+
This tranche does not implement model execution, router decisions, model download, prompt egress, network access, tool access, personal tuning, or model promotion.
44+
45+
It only hardens the carry-layer contract so future implementation work cannot treat a profile as authorization.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"schemaVersion": "v0.1",
3+
"kind": "ModelCarryAuthorizationBoundary",
4+
"boundaryId": "urn:srcos:model-carry-boundary:invalid-download",
5+
"profileRef": "urn:srcos:model-profile:local-llama32-1b",
6+
"profileKind": "LocalModelProfile",
7+
"carryScope": {
8+
"mayCarryProfile": true,
9+
"mayCarryServiceRef": true,
10+
"mayEmitEvidence": true,
11+
"mayRouteLocally": true
12+
},
13+
"authorizationBoundary": {
14+
"authorizesPromptEgress": false,
15+
"authorizesNetworkAccess": false,
16+
"authorizesToolUse": false,
17+
"authorizesModelDownload": true,
18+
"authorizesTrainingOnUserData": false,
19+
"authorizesModelPromotion": false,
20+
"authorizesLifecycleMutation": false
21+
},
22+
"decisionRefs": {
23+
"policyDecisionRefs": [],
24+
"routerDecisionRefs": [],
25+
"governanceLedgerRefs": [],
26+
"explicitPullRefs": []
27+
},
28+
"evidenceRefs": ["evidence://sourceos-model-carry/invalid/model-download"],
29+
"notes": "Invalid fixture: carry profile must not authorize model download."
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"schemaVersion": "v0.1",
3+
"kind": "ModelCarryAuthorizationBoundary",
4+
"boundaryId": "urn:srcos:model-carry-boundary:local-llama32-1b",
5+
"profileRef": "urn:srcos:model-profile:local-llama32-1b",
6+
"profileKind": "LocalModelProfile",
7+
"carryScope": {
8+
"mayCarryProfile": true,
9+
"mayCarryServiceRef": true,
10+
"mayEmitEvidence": true,
11+
"mayRouteLocally": true
12+
},
13+
"authorizationBoundary": {
14+
"authorizesPromptEgress": false,
15+
"authorizesNetworkAccess": false,
16+
"authorizesToolUse": false,
17+
"authorizesModelDownload": false,
18+
"authorizesTrainingOnUserData": false,
19+
"authorizesModelPromotion": false,
20+
"authorizesLifecycleMutation": false
21+
},
22+
"decisionRefs": {
23+
"policyDecisionRefs": [],
24+
"routerDecisionRefs": [],
25+
"governanceLedgerRefs": [],
26+
"explicitPullRefs": []
27+
},
28+
"evidenceRefs": ["evidence://sourceos-model-carry/local-llama32-1b/profile-boundary"],
29+
"notes": "This record proves the carry profile may be carried and routed locally, but does not authorize prompt egress, network, tool use, download, training, promotion, or lifecycle mutation."
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"schemaVersion": "v0.1",
3+
"kind": "ModelCarryAuthorizationBoundary",
4+
"boundaryId": "urn:srcos:model-carry-boundary:invalid-prompt-egress",
5+
"profileRef": "urn:srcos:model-profile:local-llama32-1b",
6+
"profileKind": "LocalModelProfile",
7+
"carryScope": {
8+
"mayCarryProfile": true,
9+
"mayCarryServiceRef": true,
10+
"mayEmitEvidence": true,
11+
"mayRouteLocally": true
12+
},
13+
"authorizationBoundary": {
14+
"authorizesPromptEgress": true,
15+
"authorizesNetworkAccess": false,
16+
"authorizesToolUse": false,
17+
"authorizesModelDownload": false,
18+
"authorizesTrainingOnUserData": false,
19+
"authorizesModelPromotion": false,
20+
"authorizesLifecycleMutation": false
21+
},
22+
"decisionRefs": {
23+
"policyDecisionRefs": [],
24+
"routerDecisionRefs": [],
25+
"governanceLedgerRefs": [],
26+
"explicitPullRefs": []
27+
},
28+
"evidenceRefs": ["evidence://sourceos-model-carry/invalid/prompt-egress"],
29+
"notes": "Invalid fixture: carry profile must not authorize prompt egress."
30+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env python3
2+
"""Validate SourceOS ModelCarryAuthorizationBoundary examples."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
from pathlib import Path
8+
from typing import Any
9+
10+
ROOT = Path(__file__).resolve().parents[1]
11+
SCHEMA = ROOT / "contracts" / "model-carry-authorization-boundary.schema.json"
12+
VALID = ROOT / "examples" / "model-carry-authorization-boundary.local-llama32-1b.json"
13+
INVALID_PROMPT_EGRESS = ROOT / "examples" / "model-carry-authorization-boundary.prompt-egress.invalid.json"
14+
INVALID_DOWNLOAD = ROOT / "examples" / "model-carry-authorization-boundary.download.invalid.json"
15+
16+
FORBIDDEN_AUTHORIZATIONS = (
17+
"authorizesPromptEgress",
18+
"authorizesNetworkAccess",
19+
"authorizesToolUse",
20+
"authorizesModelDownload",
21+
"authorizesTrainingOnUserData",
22+
"authorizesModelPromotion",
23+
"authorizesLifecycleMutation",
24+
)
25+
26+
27+
class ValidationError(Exception):
28+
pass
29+
30+
31+
def load_json(path: Path) -> dict[str, Any]:
32+
payload = json.loads(path.read_text(encoding="utf-8"))
33+
if not isinstance(payload, dict):
34+
raise ValidationError(f"{path.relative_to(ROOT)}: expected JSON object")
35+
return payload
36+
37+
38+
def require(condition: bool, message: str) -> None:
39+
if not condition:
40+
raise ValidationError(message)
41+
42+
43+
def validate_schema(schema: dict[str, Any]) -> None:
44+
require(schema.get("$schema") == "https://json-schema.org/draft/2020-12/schema", "schema draft mismatch")
45+
require(schema.get("type") == "object", "schema must describe object")
46+
require(schema.get("additionalProperties") is False, "schema must be closed")
47+
48+
49+
def validate_boundary(path: Path, record: dict[str, Any]) -> None:
50+
require(record.get("schemaVersion") == "v0.1", f"{path}: schemaVersion must be v0.1")
51+
require(record.get("kind") == "ModelCarryAuthorizationBoundary", f"{path}: kind mismatch")
52+
require(str(record.get("boundaryId", "")).startswith("urn:srcos:model-carry-boundary:"), f"{path}: boundaryId must be SourceOS boundary URN")
53+
require(str(record.get("profileRef", "")).startswith("urn:srcos:model-profile:"), f"{path}: profileRef must point at model profile")
54+
require(record.get("profileKind") == "LocalModelProfile", f"{path}: profileKind must be LocalModelProfile")
55+
56+
carry_scope = record.get("carryScope", {})
57+
require(carry_scope.get("mayCarryProfile") is True, f"{path}: mayCarryProfile must be true")
58+
require(carry_scope.get("mayCarryServiceRef") is True, f"{path}: mayCarryServiceRef must be true")
59+
require(carry_scope.get("mayEmitEvidence") is True, f"{path}: mayEmitEvidence must be true")
60+
61+
auth = record.get("authorizationBoundary", {})
62+
for field in FORBIDDEN_AUTHORIZATIONS:
63+
require(auth.get(field) is False, f"{path}: carry profile must not set {field}=true")
64+
65+
evidence_refs = record.get("evidenceRefs", [])
66+
require(isinstance(evidence_refs, list) and evidence_refs, f"{path}: evidenceRefs required")
67+
for ref in evidence_refs:
68+
require(isinstance(ref, str) and ref.startswith("evidence://"), f"{path}: evidenceRefs must use evidence:// refs")
69+
70+
71+
def expect_invalid(path: Path) -> None:
72+
try:
73+
validate_boundary(path.relative_to(ROOT), load_json(path))
74+
except ValidationError:
75+
return
76+
raise ValidationError(f"invalid fixture unexpectedly validated: {path.relative_to(ROOT)}")
77+
78+
79+
def main() -> int:
80+
try:
81+
validate_schema(load_json(SCHEMA))
82+
validate_boundary(VALID.relative_to(ROOT), load_json(VALID))
83+
expect_invalid(INVALID_PROMPT_EGRESS)
84+
expect_invalid(INVALID_DOWNLOAD)
85+
except (OSError, json.JSONDecodeError, ValidationError) as exc:
86+
print(f"ERR: {exc}")
87+
return 1
88+
print("Model carry authorization boundary validation passed")
89+
return 0
90+
91+
92+
if __name__ == "__main__":
93+
raise SystemExit(main())

0 commit comments

Comments
 (0)