14 KiB
mosaic cred grant / validate caller contract v1.5
Status: early binding contract for MC-CRED-01 and MB-BRAIN-01. v1.3's anonymous absence classifier was withdrawn as unsound for private users. v1.4 adopted subject-credential validation without admin visibility. v1.5 separates in-scope capability from identity measurement so correctly least-privileged tokens are not widened to service the instrument. This contract may evolve before implementation merge; incompatible changes require an explicit change notice.
Security model
- Every call carries both
--estateand--host. The configured estate-to-host mapping must match exactly. Host inference, host-adjacent fallback, and cross-estate resolution are forbidden. <identity>is always explicit. The CLI never substitutes a pane, roster, login, Unix user, or other plausible ambient identity.- The identity token and the host-bound Tea login are one provisioning unit. Minting authority reads the principal back when the invariant is created and records that binding with the token registration. Runtime validation re-measures identity only when the token already holds
read:user; it never widens scopes to make the instrument green. - Grant authority is broker/delegated-provisioner material. It is never supplied as a CLI value, environment value, or bearer token readable by the requesting agent. The broker obtains it from its protected runtime credential channel.
- Commands never print token, password, authorization header, fingerprint, partial secret, or secret-bearing error text. Structured evidence contains provider object fields and endpoint metadata only.
- Every operation opens and fsyncs a durable journal before the first mutation. Journal/audit write failure is fatal. A grant is successful only after provider read-back and a sealed journal.
Commands
mosaic cred grant <identity> \
--estate <estate> \
--host <host> \
--repo <owner/repo> \
--permission <read|write|admin> \
[--via <collaborator|team>] \
[--team <team>] \
[--read-only-control <identity>] \
[--json]
mosaic cred validate <identity> \
--estate <estate> \
--host <host> \
[--repo <owner/repo>] \
[--require <read|write|admin>] \
[--read-only-control <identity>] \
[--json]
Rules:
--via collaboratoris the default. It grants a direct repository permission and still reports the organization-membership layer.--via teamrequires--team;--teamwith collaborator mode is invalid.validate --reporeports two independent axes: capability from an in-scope repository probe, and identity binding from/useronly when authorized. Capability may beconfirmedwhile identity isnot-measured; NOT-MEASURED is neither pass nor failure.- Write validation requires a distinct known-read-only control identity, supplied explicitly or configured in the declared estate. The control identity and its read-only permission are read back from the provider on every invocation; the configured name alone is not evidence.
grantinvokes the same validation after mutation. HTTP 2xx and process exit status are never acceptance evidence.
Machine result
--json writes exactly one non-secret JSON object to stdout. Human diagnostics go to stderr. Callers must decide from outcome, never by parsing prose.
{
"schemaVersion": 1,
"operation": "grant",
"outcome": "ok",
"exitCode": 0,
"retryable": false,
"subject": {
"identity": "seat-name",
"estate": "estate-name",
"host": "git.example.invalid",
"repo": "owner/repo"
},
"mutation": "applied",
"reason": {
"code": "grant-verified",
"message": "Grant matched all provider read-backs."
},
"evidence": {
"providerIdentity": {
"login": "seat-name",
"endpoint": "GET /api/v1/user",
"contentType": "application/json"
},
"tokenCapabilities": [],
"repositoryPermission": {
"requested": "write",
"effective": "write",
"endpoint": "GET /api/v1/repos/owner/repo",
"contentType": "application/json"
},
"organizationMembership": {
"state": "present"
},
"teamMembership": {
"state": "not-applicable"
},
"writeDifferential": {
"state": "can-write",
"credentialBinding": "same-resolution",
"transportPrincipal": "seat-name",
"authenticatedReceivePack": "advertised",
"readOnlyControl": {
"identity": "read-only-control",
"providerPermission": "read",
"receivePack": "refused"
},
"unauthenticatedReceivePack": "refused",
"artifactCreated": false,
"proves": "One immutable credential resolution authenticated both the subject identity read-back and write transport; a provider-confirmed read-only principal and an unauthenticated caller were both refused.",
"doesNotProve": "A particular ref update will pass branch protection, hooks, races, or content policy."
}
},
"audit": {
"journalId": "opaque-id",
"state": "sealed"
}
}
Fields may be null only when their enclosing evidence state explains why. Missing decision-relevant fields make the result indeterminate, never ok.
Terminal classes
| Outcome | Exit | Meaning | Mutation guarantee | Caller action |
|---|---|---|---|---|
ok |
0 |
Requested property was established from provider objects and all required layers agree. | validate: none; grant: applied and read back. |
Continue. |
refused |
10 |
A complete, authoritative policy/access decision denied the request. Examples: estate-host mismatch, missing explicit identity, provider identity mismatch, explicit permission denial, or cross-estate subject. | none; refusal occurs before mutation. |
Treat as a stable denial. Do not retry without changing authority/configuration. |
error |
20 |
The command contract or local control failed before an access verdict. Examples: invalid arguments, malformed estate registry, insecure credential path, journal cannot be opened/fsynced, or internal invariant failure. | none unless mutation explicitly says unknown; unknown is never success. |
Repair the tool/configuration. Do not reinterpret as access denial. |
indeterminate |
30 |
The requested security property could not be evaluated completely or evidence disagreed. Examples: provider unavailable, wrong content type/shape, stale or absent scope read-back, permission and receive-pack disagreement, missing post-grant read-back, or unknown mutation acknowledgement. | none, applied, or unknown, stated explicitly. Never infer. |
Fail closed at the calling gate. Investigate/re-evaluate; do not label the subject refused. |
Parsing/usage errors emitted by Commander remain exit 2 and do not produce a broker verdict. Callers should treat them as integration defects, not access decisions.
Refusal object
A refusal is intentionally recognizable without prose:
{
"schemaVersion": 1,
"operation": "validate",
"outcome": "refused",
"exitCode": 10,
"retryable": false,
"subject": {
"identity": "external-seat",
"estate": "homelab",
"host": "git.example.invalid",
"repo": "owner/repo"
},
"mutation": "none",
"reason": {
"code": "no-token-for-identity",
"message": "The explicit identity has no credential in the declared estate."
},
"evidence": {
"providerIdentity": null,
"tokenCapabilities": [],
"repositoryPermission": null,
"organizationMembership": null,
"teamMembership": null,
"writeDifferential": null
},
"audit": {
"journalId": "opaque-id",
"state": "sealed"
}
}
The git credential helper and API resolver must map the same subject/estate/host failure to the same reason.code and terminal class. MB-BRAIN-01 may assert this parity. A caller does not need to know which resolver path was used.
Required reason codes
Stable v1 codes:
- refusal:
identity-required,estate-required,estate-host-mismatch,cross-estate-resolution,no-token-for-identity,tea-login-missing,tea-login-host-mismatch,provider-identity-mismatch,credential-rejected,permission-denied,organization-membership-required,team-membership-required - error:
invalid-input,estate-registry-invalid,insecure-credential-source,journal-unavailable,internal-invariant - indeterminate:
provider-unavailable,identity-not-visible,identity-not-measured,identity-not-found,unexpected-content-type,unexpected-provider-shape,scope-not-evaluable,permission-evidence-disagrees,transport-principal-mismatch,read-only-control-invalid,readback-missing,mutation-state-unknown
provider-unavailable means no usable provider answer was available. identity-not-measured means /user was scope-forbidden while an in-scope repository probe confirmed the credential capability; it is indeterminate only for the identity axis and must not be represented as a dead credential. identity-not-visible and identity-not-found are reserved for the unimplemented external inventory capability. credential-rejected means the provider rejected the credential itself (Gitea 401), which is a stable refused outcome. A 403 on /user is not credential rejection when an in-scope probe succeeds.
No anonymous or visibility-unprivileged 404 is admissible evidence of absence. identity-not-found requires, in the same invocation: (1) the visibility credential's own /user object read back as the configured authority with provider-admin visibility; (2) target lookup performed with that same authority; (3) a known-present PRIVATE control returning JSON 200 with matching login and visibility=private; and (4) a generated absent negative control returning JSON 404 under that same authority. Missing authority or any non-discriminating control yields identity-not-visible, never absence. A public positive control cannot certify private subjects.
No currently implemented operation may emit identity-not-found: the required governed inventory capability was deliberately declined and runtime validation must not acquire standing admin visibility. For validate, /user 401 means credential-rejected; /user 403/404 triggers the in-scope capability probe and, when that succeeds, identity is identity-not-measured; JSON 200 with a mismatched login is a binding refusal. A future inventory operation must meet every precondition above and receive an explicit privilege decision before making identity-not-found reachable.
Unknown future reason codes must still carry one of the four stable outcome values.
Side-effect-free write differential
For Gitea v1, validate --repo resolves the subject credential exactly once into an immutable in-memory credential handle. The provider /user read-back, authenticated repository object, and Git smart-HTTP git-receive-pack advertisement all consume that same handle; callers may not perform independent lookups for those steps. The command also probes a separately resolved, provider-confirmed read-only control principal and repeats the request unauthenticated.
can-write requires all of the following:
- provider
/userlogin obtained with the subject credential handle equals<identity>; - authenticated repository object obtained with that same handle reports write-capable permission;
- receive-pack obtained with that same handle returns the exact advertisement content type and protocol preamble;
- the transport evidence records the same declared principal as the identity read-back; any handle/principal seam disagreement is
transport-principal-mismatchand thereforeindeterminate, never refused; - a distinct known-read-only credential resolves to its declared control identity, its provider repository object reports no write permission, and receive-pack is refused;
- the unauthenticated control is refused and does not return a receive-pack advertisement;
- estate, host, and repository in every request equal the declared subject.
The read-only control varies the mechanism under accusation: principal selection. The unauthenticated arm remains as a separate control proving authentication is required; it cannot establish which principal authenticated the subject probe. A missing, write-capable, identity-mismatched, or otherwise invalid read-only control makes the result indeterminate.
No ref is updated and no repository artifact is created. This proves that the declared subject credential—not merely some authenticated credential—can enter the write transport for that repository, while a provider-confirmed read-only principal and an unauthenticated caller cannot. It does not prove any specific branch update would survive branch protection, hooks, concurrent changes, or content policy.
Grant read-back
A collaborator grant is accepted only when the provider returns the named collaborator permission and the subject credential independently reads the repository with matching effective permission. A team grant additionally requires provider-read-back of organization membership, team membership, team repository attachment, and effective subject permission. Token capability, repository permission, and organization/team role are reported as separate layers; no layer substitutes for another.