policy: policy records, attached policies, tangled-vouch
- a market policy ALWAYS runs, both sides, both lanes (scope + full), every decision. no skip paths
- the requester publishes the workflow it runs as a record; the RFP strongRefs it; the bidder runs it next to its own
- caching lives inside the engine run, never across parties, never a skip
- index: README.md. sequence steps: S1 (vouches), S3 (record + RFP), S4 (bidder runs), S5 (requester runs)
Records And Fields
| NSID / field | type | rkey | written by | read by | Go | step |
|---|---|---|---|---|---|---|
computer.socialweb.temp.policy.ghalite | record (no lexicon file) | TID (createRecord without rkey); one per contract, kept in state.Policy | requester | bidder (via the RFP strongRef), requester (re-check) | internal/market/requester/policy.go BuildPolicyRecord, internal/market/requester/runner.go, internal/market/participant/policyrecord.go | S3 |
market.rfp policies[] | array of com.atproto.repo.strongRef | - | requester | bidder | internal/atproto/market/records.go BuildRFP, CreateRFP | S3 |
computer.socialweb.infra#contract policy, policyArgs | strings | - | owner | requester (via the ComputeContract) | internal/market/requester/runner.go | S2 |
sh.tangled.graph.vouch | record (outside) | subject DID | operators, users | both policies (trust snapshot) | internal/policyengine/trust/trust.go | S1 |
computer.socialweb.temp.policy.typescript | record (no lexicon file) | - | nobody | denied | internal/policyengine/registry/registry.go | - |
com.publicdomainrelay.temp.market.policies.denoWorker | record (no lexicon file) | - | nobody | denied | internal/policyengine/registry/registry.go | - |
computer.socialweb.temp.policy.wasm | test fixture only | - | - | denied as an unknown type | internal/policyengine/controller/records.go | - |
- no policy field on
market.bid,market.offeringorcompute.*. the bidder's own Bid object keepsspec.policy {attached, verdict}in kcp only
computer.socialweb.temp.policy.ghalite
| field | value |
|---|---|
$type | computer.socialweb.temp.policy.ghalite |
name | the workflow name, e.g. requester-tangled-vouch |
workflow | the full YAML text of the bundled workflow (or its name when there is no text) |
args | map[string]string, from policyArgs |
requester | the requester's market DID |
perspective | requester |
policies | [{name, args?}] |
createdAt | datetime |
PolicyRecordWorkflowreadsworkflowas a YAML string or an object and denies any other$type(internal/market/participant/policyrecord.go)- typescript and denoWorker executors are registered as deny ("the policy engine is gha-lite only")
- OAuth: requester and bidder sets carry create (+ update on requester) for
ghaliteandtypescript; none for denoWorker (outside.md)
Infra Contract Fields
policy: the workflow name. resolution:cmp.Or(contract.Spec.Policy, cfg.PolicyWorkflow, DefaultPolicyWorkflow)PolicyWorkflowNamenormalizes:""->requester-tangled-vouch;only-me->requester-only-me;mutuals->requester-mutuals;tangled-vouch->requester-tangled-vouch- the name must be a bundled workflow, else the contract fails before the RFP
policyArgs: a JSON object, parsed tomap[string]string(internal/market/participant/policy.goParsePolicyArgs)
RFP Policy StrongRefs
sequenceDiagram
autonumber
participant R as requester
participant RP as requester repo
participant B as bidder
participant E as policy engine (bidder)
R->>R: policy name from Infra contract / ComputeContract
R->>RP: S3 createRecord policy.ghalite {workflow text, args}
R->>RP: getRecord it back: own repo, cid, name, body == bundled workflow
R->>RP: S3 market.rfp {policies: [strongRef]}
RP-->>B: market.rfp
B->>E: own workflow (bidder-tangled-vouch), scope lane (DIDs only)
B->>RP: getRecord each policies[] ref
B->>E: own workflow + each attached workflow, full lane
E-->>B: allow only if all allow
B->>B: S4 bid or no bid
- requester write path:
runner.gostepcreatingRfp:BuildPolicyRecord->CreateRecord->attachedPolicy->rfpOptions.Policies->market.CreateRFP - requester re-check
attachedPolicy(internal/market/requester/policy.go): the record is in the requester's own repo, the cid matches, the name matches the contract, the body equals the bundled workflow byte for byte.rfpPolicyRefrequires exactly one policy - bidder:
EvaluatePolicies(internal/market/bidder/policy.go) runs its own workflow (defaultbidder-tangled-vouch), then each RFP ref viaRepoStore.Resolve(local getRecord or a foreign resolver),PolicyRecordWorkflow,runs.Request{Workflow, Source: uri}. first deny wins - the bidder runs the attached workflow with
perspective=bidder(internal/market/bidder/bids.go) - a restarted runner reuses
state.Policy; it does not write a second record
Lanes And Modes
| lane | input | where |
|---|---|---|
| scope | DIDs only, RFP nil, mode: scope | bidder bids.go, internal/policyengine/executor/ghalite.go Scope |
| full | self, subject, counterparty, root requester DIDs, rfp, bid, accept, args, mode: full | bidder bids.go fullInput, adapters.go; ghalite.go Execute |
- executor:
GhaLite(internal/policyengine/executor/ghalite.go), default timeout 30 s - run path:
runs.Client.Runcreates a PolicyWorkflowRun and waits (internal/policyengine/runs/client.go) - missing engine, evaluator or a non-boolean
allow= deny
Caching (inside the run)
| cache | key | TTL |
|---|---|---|
| trust snapshot seed | trust/<counterpartyDid> | per run |
| record | rec/<uri>/<cid> (internal/policyengine/cache/cache.go) | - |
| verdict | policy/<name>/<rfp>|<bid>|<accept>[:argsHash] | 30 s default |
| scope verdict memo | internal/policyengine/controller/scopememo.go | 30 s, plus a negative TTL |
| vouch, operator lookups | trust.Resolver ttlcache | 5 min, errors 5 s |
Bundled Workflows
- embedded:
internal/policyengine/bundled-actions/embed.go(workflows/,tangy/), looked up byWorkflow(name)andLookup(uses) - every workflow is a
workflow_dispatchwrapper with inputsself-did,rfp,bid,accept,policy-args,mode,counterparty-did,perspective,subject-did,root-requester-did,cache-key; it callstangy/policy-<name>@v1and outputsallow,violations
| workflow | kind | decides |
|---|---|---|
bidder-tangled-vouch | trust (bidder default) | the requester or its operator is vouched by the bidder or its operator |
requester-tangled-vouch | trust (requester default) | the same, for the bidder, from the requester side |
bidder-mutuals, requester-mutuals | trust | operators follow each other on Bluesky |
bidder-only-me, requester-only-me | trust | the counterparty resolves to the own operator |
open | trust | no restriction. only when named, never prod |
bid-payload | work (requester) | the bid payload NSID is in allowedPayloadNsids (default market.bids.free) |
under-4-cpus | work | the VM asks at most maxCpus vCPUs |
- the action infers its perspective from the
bidder-prefix unless theperspectiveinput says otherwise - defaults:
api/v1alpha1/types_computevm.goBidderDefaultPolicy,RequesterDefaultPolicy
Tangled-Vouch
- default on both sides everywhere;
openonly if the user sets it - a vouch is
sh.tangled.graph.vouchat rkey = the subject DID in the voucher's repo; it counts when it exists andkind != "denounce": outside.md - both directions are needed for a deal: the bidder side vouches for the requester side and the requester side for the bidder side
flowchart LR
SELF["self"] --> T["trusted = self + operator(self)"]
CP["counterparty"] --> OPC["operator(counterparty) via identity.md"]
T --> Q{"one getRecord per pair: trusted vouches counterparty or operator(counterparty)?"}
OPC --> Q
Q -->|yes| ALLOW["allow"]
Q -->|operator(counterparty) is trusted| ALLOW
Q -->|no| DENY["deny"]
- snapshot:
trust.BuildSnapshotWith(internal/policyengine/trust/trust.go); depth 1, no transitive walk - operator resolution (claims, associations, mirror proofs): identity.md
- who writes vouches: market networks declare operator vouches both ways (
internal/app/marketnetwork/vouches.go, composeApplyOperatorVouch); people write them from any client. the requester OAuth scope offers vouch write as an optional scope - requester discovery lists the vouches of its trusted DIDs (
internal/market/requester/discovery.govouchedBy, a listRecords that skipsdenounceand takes rkeys startingdid:)
See Also
- README.md, identity.md, outside.md, market.md
- ../SYSTEM.md,
docs/ARCHITECTURE.md(open by heading: policy engine)