Gate clients
doublegate_sdk connects to a gate only when you construct a transport and
call a method. Nothing is discovered from the environment; no credential is
read on import; nothing retries on its own. Two clients share one transport
layer and one operation table (ADR-0074):
| client | who | what it can do | what it can never do |
|---|---|---|---|
KnowledgeClient |
an agent, a connector, an application | remember, learn, propose a skill or a bundle, recall, read status/why/tip/relations/approval/comments, annotate, object, resolve, defer, rank or hide another writer's row | sign, promote, demote, reject, relate, ban, mint an operator proof |
CurationClient |
the operator (a human, or a script the human runs) | approve, hold, reject, veto, promote, relate/supersede, override the tip, rank, hide, ban, away/back, submit, semantic review, the organization's keys | act without the operator's signature |
The gate is not callable by the thing being gated. The knowledge client's
transport refuses a deciding verb before any I/O; the gate refuses it again
from the identity it derived off the connection (I2). A successful proposal is
a receipt, not an admission: every result carries proposals_are_admission = False.
These two clients speak the gate's dg.* verbs over its Unix socket or its
POST /rpc door. The third door, POST /mcp, is the tool catalog an MCP host
sees; doublegate_sdk.client (Gate client (HTTP/MCP)) speaks
it with HttpMcpTransport, exposes only the tools the maintained client tier
serves, and raises the same GateError.
Discover the API
python -m doublegate_sdk describe-client prints both clients' methods from
their live signatures, the operation table (rpc, mutates, proof, role,
scope), the three scopes, the error kinds and the two code tables. It
describes this SDK. KnowledgeClient.describe() is what a running gate
answers (dg.describe), parsed into Capabilities.
Transports and scopes
from doublegate_sdk.transport import UnixSocketTransport, HttpTransport
local = UnixSocketTransport('/explicit/gate/daemon.sock', timeout=5, scope='knowledge')
console = HttpTransport('http://127.0.0.1:8480', bearer=console_token, scope='knowledge')
org = HttpTransport('https://org.example', bearer=admin_key, scope='curation')
scope is the widest class of verb the transport will emit: read (the
default; every write is writes_disabled), knowledge (an agent's verbs), or
curation (the operator's). A verb outside the scope raises
GateError('forbidden_operation') before a connection is opened; an unknown
verb is a ValueError. Both transports share one deadline for connect, send
and every read (10 s by default), a 1 MiB request cap and a 1 MiB response cap,
duplicate-key rejection and the same code table. with_timeout(seconds)
returns a copy for a short liveness probe. A caller-supplied GateTransport
(call(method, params) -> dict) owns its own allowlist.
HttpTransport posts one JSON-RPC message to {base}/rpc with a bearer: the
client gate's console token on its loopback listener, or an organization
gate's admin key. A JSON-RPC refusal rides a 200; the door's own refusals are
HTTP statuses (auth 401, scope 403, request_too_large 413, busy 429/503
with Retry-After kept as retry_after_ms). It is not an SSO adapter and it
adds no authorization: the gate still checks every request.
Errors
GateError.kind is what you act on. code keeps the server's JSON-RPC number;
retry_after_ms is set on busy; outcome_unknown is true when a write may
have been retained although the reply was lost; detail carries the server's
message, capped at 512 characters, and never appears in str(exc).
| kind | when |
|---|---|
unavailable, timeout, unsupported_transport |
no gate answered in time |
invalid_response, response_too_large, request_too_large |
the frame, not the gate |
writes_disabled, forbidden_operation, proof_required, page_limit |
refused on this side, before I/O |
identity, invalid_params, unsupported_operation, busy, banned, refused, remote_error |
the gate's answer (-32000, -32600/-32602, -32601, -32006, -32009, -32010..-32014, other) |
auth, scope |
the HTTP /rpc door (401, 403) |
unauthorized, forbidden, tool_error, redirect_refused |
the HTTP /mcp door (mcp-client) |
The same two tables live in the constellation app's rpc.ts; change both or neither.
The knowledge client
from doublegate_sdk.knowledge import KnowledgeClient
agent = KnowledgeClient(local)
if agent.present():
caps = agent.describe() # Capabilities: verbs, params, content types
memo = agent.remember('The lab labels basalt by collection date.',
content_type='memory', trust_class='T-4', source_uri='agent://field/42')
fact = agent.learn('Basalt labels are durable.', source_uri='agent://field/42',
evidence=[memo.artifact_id], kind='derived_fact')
agent.annotate(fact.artifact_id, 'derived from the 2026-09 survey', kind='note')
row = agent.wait_for(fact.artifact_id, {'ACTIVE', 'REJECTED'}, timeout=30)
hits = agent.recall('basalt', limit=5, include_provisional=True)
rememberproposes one artifact: bytes travel base64 and unchanged; the gate scans before it answers and holds the row (L1_SCANNEDorL1B_FLAGGED). No writer identity is ever sent.learnis a belief distilled from evidence:kindisderived_fact,summaryormemory;evidencebecomesderives_from, which is provenance and never hides a parent; the default trust class isT-5, a proposal about the world.replaces=<artifact id>adds that claim to the provenance and leaves a note "proposes: supersedes …" on the new row. Only a gate decision replaces belief;supersedesitself is the operator's relation.propose_skill,propose_prompt_templateandpropose_bundle(a ZIP, inspected locally first, thendg.ingest_bundlebeside its fetch record).recallalways excludes the caller's own unreviewed rows; provisional, superseded and hidden rows only on explicit request, each hit keeping its flags. No score is relabelled as trust.annotate,raise_objection,resolve,retract_commentare remarks on the record (ADR-0072). Only a human's objection holds a promotion; an agent's is a remark the reviewer will see.rankandhideweight a row for an audience; the gate refuses them on the caller's own rows (I2).inventory/inventory_pageslist retained metadata with scope and coverage, bounded bymax_pages; presence never implies admission.
The curation client
from doublegate_sdk.curation import CurationClient
from doublegate_sdk.proof import ServerMinted, SignerProof
# on the client gate's host, the daemon mints the proof from the key it holds
operator = CurationClient(UnixSocketTransport(sock, scope='curation'), ServerMinted())
# anywhere else, the operator signs the nonce with a function the SDK never sees inside
operator = CurationClient(org, SignerProof(sign=my_ed25519_key.sign))
operator.approve(aid, note='matches the survey') # dg.sign {promote: true}
operator.veto(other, 'contradicts the 2025 audit') # dg.demote
operator.supersede(new=aid, old=other) # dg.relate supersedes
operator.reject(third, 'duplicate', in_favour_of=aid)
Every operator verb fetches a fresh dg.challenge nonce, has the provider
sign it, and sends {nonce, sig} with the call; nonces are single-use, so
approve is one proof for the signature and the promotion it was for.
Without a provider the operator verbs raise proof_required before I/O.
rank, hide and comment attach a proof when a provider is present. The
SDK never loads keys/operator.ed25519 and never depends on a crypto
library: the signer is yours. operator.knowledge reads the same gate the
agent's way.
The organization gate's REST doors
doublegate_sdk.org_rest moves the submission body that
doublegate_sdk.submission encodes: put_submission returns a
SubmissionOutcome whose result is one of accepted (202/200), retry
(503/429 with the door's delay, or an unexpected status), stopped (401/403:
a key problem does not become correct by waiting) or refused (400/413/422:
this body, not the key). pull_outcomes pages GET /outcomes by cursor.
Policy — backoff, cursors, what to do about retry — stays with the caller.
Verified scope
Unit tests drive both transports against a real AF_UNIX listener and a
loopback http.server: the allowlist before I/O, the shared deadline, byte
caps, malformed and duplicate-key replies, lost-reply outcomes, the code
tables, every client method against a recording transport, the proof flow for
each operator verb. examples/verify_knowledge_lifecycle.py runs a real Client
Gate in-process with the deterministic StubBackend: proposal → held →
duplicate → admission → provisional recall → a learning with provenance. It is
not live-model, cross-principal, Windows or organization-delivery evidence.
doublegate_sdk.knowledge
The knowledge client: what an agent may do with a gate (ADR-0074 d2, d4).
Propose, learn, ask, annotate, weight for an audience. Never decide: no
method here signs, promotes, demotes, rejects or relates, and the transport
refuses those verbs before I/O. Identity is the connection's — nothing here
takes or sends a writer identity (I2). A successful proposal is a receipt,
not an admission; proposals_are_admission is False on every result.
LEARNING_KINDS
module-attribute
COMMENT_KINDS
module-attribute
Proposal
dataclass
The gate's receipt for one proposal. state is the gate's word, not ours.
Source code in src/doublegate_sdk/knowledge.py
raw
class-attribute
instance-attribute
from_response
classmethod
Source code in src/doublegate_sdk/knowledge.py
Learning
dataclass
Bases: Proposal
A learning: content plus the evidence it derives from. proposed_supersedes
names the claim it would replace; only a gate decision replaces belief (ADR-0049 d5).
Source code in src/doublegate_sdk/knowledge.py
KnowledgeClient
An agent's view of a gate through an explicit transport of scope read or knowledge.
Source code in src/doublegate_sdk/knowledge.py
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |
present
Is a gate answering? Never raises; a refusal or an outage is False.
Source code in src/doublegate_sdk/knowledge.py
ping
describe
status
why
tip
relations
approval
node
comments
Source code in src/doublegate_sdk/knowledge.py
pending
inventory
Source code in src/doublegate_sdk/knowledge.py
inventory_pages
inventory_pages(*, limit: int = 100, max_pages: int = 10, **filters: Any) -> Iterator[InventoryPage]
Source code in src/doublegate_sdk/knowledge.py
recall
recall(query: str, *, limit: int = 10, spaces: list[str] | None = None, include_provisional: bool = False, include_superseded: bool = False, include_hidden: bool = False, audience: str | None = None, mode: str | None = None) -> dict[str, Any]
Served knowledge. The caller's own unreviewed rows are always excluded; provisional, superseded and hidden rows only on explicit request, and each hit keeps its flags — no score is relabelled as trust.
Source code in src/doublegate_sdk/knowledge.py
remember
remember(content: str | bytes, *, content_type: str, source_uri: str, trust_class: str = 'T-1', space: str = 'main', derives_from: Sequence[str] = ()) -> Proposal
Propose one artifact. Bytes travel base64 and unchanged; the gate scans before it answers and holds the row until a reviewer that did not write it promotes it. Never retried; never carries an identity.
Source code in src/doublegate_sdk/knowledge.py
learn
learn(text: str, *, source_uri: str, kind: str = 'derived_fact', evidence: Sequence[str] = (), space: str = 'main', trust_class: str = 'T-5', replaces: str | None = None) -> Learning
A learning: a claim distilled from evidence, proposed with its provenance.
evidence becomes derives_from (never hides a parent, ADR-0035 d1).
replaces names the claim this one should supersede: it joins the
provenance and a note is left on the new row saying so, because
only a gate decision replaces belief and supersedes is the
operator's relation (ADR-0049 d5; GAPS 20). Default T-5: a
sentence a model wrote is a proposal about the world, not a fact.
Source code in src/doublegate_sdk/knowledge.py
propose_skill
propose_skill(text: str, *, source_uri: str, space: str = 'main', trust_class: str = 'T-5', derives_from: Sequence[str] = ()) -> Proposal
Source code in src/doublegate_sdk/knowledge.py
propose_prompt_template
propose_prompt_template(text: str, *, source_uri: str, space: str = 'main', trust_class: str = 'T-5', derives_from: Sequence[str] = ()) -> Proposal
Source code in src/doublegate_sdk/knowledge.py
propose_bundle
propose_bundle(archive: bytes, *, fetch_artifact_id: str, source_uri: str, manifest: dict[str, Any] | None = None, space: str = 'main') -> dict[str, Any]
A skill bundle: the archive is inspected here first (skill_bundle),
then handed to dg.ingest_bundle beside the fetch record it derives
from. The gate decides the class; no trust class travels.
Source code in src/doublegate_sdk/knowledge.py
annotate
annotate(artifact_id: str, body: str, *, kind: str = 'note', reply_to: str | None = None) -> dict[str, Any]
Source code in src/doublegate_sdk/knowledge.py
raise_objection
An objection on the record. Only a human's objection holds promotion (ADR-0072); an agent's is a remark the reviewer will see (GAPS 21).
Source code in src/doublegate_sdk/knowledge.py
resolve
retract_comment
Source code in src/doublegate_sdk/knowledge.py
defer
rank
Source code in src/doublegate_sdk/knowledge.py
hide
hide(artifact_id: str, *, reason: str = '', audience: str = 'all', lift: bool = False) -> dict[str, Any]
Source code in src/doublegate_sdk/knowledge.py
wait_for
wait_for(artifact_id: str, states: Collection[str], *, timeout: float = 30.0, interval: float = 0.5) -> dict[str, Any]
Poll status until the row is in one of states or the deadline
passes; returns the last row either way, with reached saying which.
Source code in src/doublegate_sdk/knowledge.py
as_json
Canonical text for a structured learning body: sorted keys, no NaN, unicode kept.
doublegate_sdk.curation
The curation client: what the operator does (ADR-0074 d2, d3).
Every decision here is the human's own signature: the transport must be
curation-scoped and the client carries a ProofProvider that mints one
single-use operator proof per verb (ADR-0028 d1). Admin scope on an
organization gate is not operator standing (ADR-0040): without a provider
the operator verbs are refused before I/O with proof_required.
RELATIONS
module-attribute
RELATIONS: frozenset[str] = frozenset({'supersedes', 'restates', 'corrects', 'contradicts', 'extends'})
DECISIONS
module-attribute
CurationClient
The operator's view of a gate: decisions, relations, serving policy, keys.
Source code in src/doublegate_sdk/curation.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | |
sign
Source code in src/doublegate_sdk/curation.py
approve
Sign promote and promote under one proof; a promotion the boundary
refuses is reported in promote.refused and the signature stands.
Source code in src/doublegate_sdk/curation.py
hold
clear_finding
reject
Source code in src/doublegate_sdk/curation.py
veto
The human veto of an admitted row: dg.demote (ADR-0073). Ends the
claim; keeps the content.
Source code in src/doublegate_sdk/curation.py
promote
promote(artifact_id: str, *, importance: float | None = None, supersedes: str | None = None) -> dict[str, Any]
Source code in src/doublegate_sdk/curation.py
scan
grade
One model call per stance on one row; the gate engine, not this client, judges.
grade_pending
settle_pending
Promote what quorum already settled; a row under an open human objection stays (ADR-0072).
semantic_review
relate
Source code in src/doublegate_sdk/curation.py
supersede
override_tip
Source code in src/doublegate_sdk/curation.py
rank
Source code in src/doublegate_sdk/curation.py
hide
hide(artifact_id: str, *, reason: str = '', audience: str = 'all', lift: bool = False) -> dict[str, Any]
Source code in src/doublegate_sdk/curation.py
ban
ban(kind: str, subject: str, *, reason: str = '', until_ms: int | None = None, lift: bool = False) -> dict[str, Any]
Source code in src/doublegate_sdk/curation.py
bans
away
Source code in src/doublegate_sdk/curation.py
back
defer
submit
submit_drain
review
keys
dg.keys: list | show | issue | revoke. Issue and revoke are the
operator's; a client gate answers unsupported_operation.
Source code in src/doublegate_sdk/curation.py
doublegate_sdk.transport
Two transports, one framing, one allowlist (ADR-0074 d1, d6).
No connection on import or construction; no discovery, credentials, retries
or alternate endpoints. The transport's scope is the widest class of
verb it will emit — checked before any I/O — and a refusal here is a
courtesy: the receiving gate refuses regardless, from the identity it
derived off the connection (I2). A caller-supplied GateTransport owns
its own allowlist.
Both transports here carry dg.* verbs: UnixSocketTransport over the
daemon socket, HttpTransport over POST /rpc. The third door,
POST /mcp (the MCP tool catalog), is doublegate_sdk.client.HttpMcpTransport
behind doublegate_sdk.client.McpTransport; same shape, different vocabulary.
GateTransport
Bases: Protocol
Explicit operation transport; the receiver remains authoritative.
Source code in src/doublegate_sdk/transport.py
call
Source code in src/doublegate_sdk/transport.py
UnixSocketTransport
Bases: _Bounded
Bounded local JSON-RPC over the gate's own socket, newline-framed.
Source code in src/doublegate_sdk/transport.py
with_timeout
call
Source code in src/doublegate_sdk/transport.py
HttpTransport
Bases: _Bounded
The same message over POST {base}/rpc with a bearer: the client
gate's console token on its loopback listener, an organization gate's
admin key, or an assertion an agent mints from its own key (ADR-0080 d4).
bearer is the string itself or a zero-argument callable returning the
current one, called once per request — the shape a short-lived assertion
needs, re-minted by its holder before it expires. JSON-RPC refusals ride a
200; the door's own refusals are HTTP statuses (401 auth, 403 scope, 413,
429/503 with Retry-After).
Source code in src/doublegate_sdk/transport.py
with_timeout
call
Source code in src/doublegate_sdk/transport.py
encode_request
One JSON-RPC 2.0 message, id 1, bounded.
Source code in src/doublegate_sdk/transport.py
decode_response
The result of a JSON-RPC reply, or the refusal it carried as a GateError.
Source code in src/doublegate_sdk/transport.py
doublegate_sdk.proof
The operator proof seam (ADR-0074 d3).
An operator verb carries {nonce, sig}: a fresh dg.challenge nonce and
the operator key's signature over it (ADR-0028 d1). The SDK never holds that
key. It asks a ProofProvider — a caller-supplied signer, or the client
daemon's own dg.operator_proof on the host where the key lives — and
forwards what it gets (ADR-0040: wrappers forward proof, they do not
synthesize it). Nonces are single-use: one proof per verb, minted just
before the call.
OperatorProof
ProofProvider
SignerProof
Fetch a nonce from the gate and sign it with the caller's function
(bytes -> bytes, a detached Ed25519 signature). The key stays with the caller.
Source code in src/doublegate_sdk/proof.py
prove
Source code in src/doublegate_sdk/proof.py
ServerMinted
dg.operator_proof: the client daemon signs the nonce with the key on
its host. Presence on that host is the whole authorization; a gate whose
key is unreadable, or an organization gate, refuses and this says so.
Source code in src/doublegate_sdk/proof.py
prove
Source code in src/doublegate_sdk/proof.py
doublegate_sdk.capabilities
What a running gate says it answers (dg.describe), parsed once.
The catalog is the schema (ADR-0044): a client learns a verb's presence,
its parameters and whether this build dispatches it from the gate itself,
never from prose. content_types comes from the structured enum on
dg.ingest's content_type parameter; a gate that does not publish one
yields an empty set, and a caller must not read that as "accepts anything".
VerbInfo
dataclass
Source code in src/doublegate_sdk/capabilities.py
Capabilities
dataclass
Source code in src/doublegate_sdk/capabilities.py
from_describe
classmethod
Source code in src/doublegate_sdk/capabilities.py
dispatches
accepts_content_type
doublegate_sdk.errors
One error base, one gate vocabulary (ADR-0074 d5).
DoublegateError is the base every SDK failure shares, so a caller can catch
the SDK without swallowing unrelated application exceptions. GateError is
the one raised on a wire: kind is what a caller acts on; code is the
server's JSON-RPC number or HTTP status when there was one; detail carries
the server's message, capped, and never enters str(exc) so a log line
cannot leak what a gate said about a held row. The two tables here are the
same ones the constellation app's rpc.ts uses; change both or neither.
CODE_KINDS
module-attribute
CODE_KINDS: dict[int, str] = {-32000: 'identity', -32001: 'request_too_large', -32002: 'request_too_large', -32600: 'invalid_params', -32602: 'invalid_params', -32601: 'unsupported_operation', -32006: 'busy', -32009: 'banned', -32010: 'refused', -32012: 'refused', -32013: 'refused', -32014: 'refused'}
HTTP_KINDS
module-attribute
HTTP_KINDS: dict[int, str] = {401: 'auth', 403: 'scope', 413: 'request_too_large', 429: 'busy', 503: 'busy'}
KINDS
module-attribute
KINDS: frozenset[str] = frozenset({'unavailable', 'timeout', 'unsupported_transport', 'invalid_response', 'request_too_large', 'response_too_large', 'writes_disabled', 'forbidden_operation', 'page_limit', 'proof_required', 'redirect_refused', 'identity', 'invalid_params', 'unsupported_operation', 'busy', 'banned', 'refused', 'remote_error', 'auth', 'scope', 'unauthorized', 'forbidden', 'tool_error'})
DoublegateError
Bases: Exception
Catch SDK failures without swallowing unrelated application exceptions.
Concrete exceptions retain their own safe diagnostic, protocol code and
established ValueError/RuntimeError compatibility. kind names the category;
specialized fields provide details without parsing human-readable text.
Source code in src/doublegate_sdk/errors.py
GateError
Bases: DoublegateError, RuntimeError
A fixed diagnostic. str(exc) is the kind (and code); the server's text is detail.
Source code in src/doublegate_sdk/errors.py
kind_of_code
doublegate_sdk.operations
The operation table: every dg.* verb of both gate catalogs, with what a
call needs (ADR-0074 d1). The catalog is the schema (I7); this table is its
client-side reading and the allowlist every transport enforces before I/O.
scope is the widest client that may emit the verb:
read— answers questions, changes nothing;knowledge— what an agent may do: propose, annotate, weight for an audience; never decide (the gate is not callable by the thing being gated);curation— the operator's verbs, most of them under an operator proof.
OPERATIONS
module-attribute
OPERATIONS: dict[str, Operation] = {op.rpc: op for op in (_op('dg.ping', mutates=False, scope=READ), _op('dg.describe', mutates=False, scope=READ), _op('dg.status', mutates=False, scope=READ), _op('dg.pending', mutates=False, scope=READ), _op('dg.inventory', mutates=False, scope=READ), _op('dg.recall', mutates=False, scope=READ), _op('dg.why', mutates=False, scope=READ), _op('dg.audit', mutates=False, scope=READ), _op('dg.tip', mutates=False, scope=READ), _op('dg.relations', mutates=False, scope=READ), _op('dg.approval', mutates=False, scope=READ), _op('dg.graph', mutates=False, scope=READ), _op('dg.node', mutates=False, scope=READ), _op('dg.comments', mutates=False, scope=READ), _op('dg.review', mutates=False, scope=READ), _op('dg.review_runs', mutates=False, scope=READ), _op('dg.engagement', mutates=False, scope=READ), _op('dg.bans', mutates=False, scope=READ), _op('dg.settings', mutates=False, scope=READ), _op('dg.verify', mutates=False, scope=READ), _op('dg.challenge', mutates=False, scope=READ), _op('dg.ingest', mutates=True, scope=KNOWLEDGE), _op('dg.ingest_bundle', mutates=True, scope=KNOWLEDGE), _op('dg.comment', mutates=True, proof='optional', scope=KNOWLEDGE), _op('dg.comment_retract', mutates=True, proof='optional', scope=KNOWLEDGE), _op('dg.defer', mutates=True, scope=KNOWLEDGE), _op('dg.rank', mutates=True, proof='optional', scope=KNOWLEDGE), _op('dg.hide', mutates=True, proof='optional', scope=KNOWLEDGE), _op('dg.crawler_policy', mutates=True, proof='operator', scope=KNOWLEDGE), _op('dg.operator_proof', mutates=False, proof='presence', scope=CURATION), _op('dg.sign', mutates=True, proof='operator', scope=CURATION), _op('dg.promote', mutates=True, proof='operator', scope=CURATION), _op('dg.demote', mutates=True, proof='operator', scope=CURATION), _op('dg.reject', mutates=True, proof='operator', scope=CURATION), _op('dg.relate', mutates=True, proof='operator', scope=CURATION), _op('dg.override_tip', mutates=True, proof='operator', scope=CURATION), _op('dg.ban', mutates=True, proof='operator', scope=CURATION), _op('dg.away', mutates=True, proof='operator', scope=CURATION), _op('dg.back', mutates=True, proof='operator', scope=CURATION), _op('dg.semantic_review', mutates=True, scope=CURATION), _op('dg.scan', mutates=True, scope=CURATION), _op('dg.scan_pending', mutates=True, scope=CURATION), _op('dg.grade', mutates=True, scope=CURATION), _op('dg.grade_pending', mutates=True, scope=CURATION), _op('dg.review_pending', mutates=True, scope=CURATION), _op('dg.settle_pending', mutates=True, scope=CURATION), _op('dg.submit', mutates=True, role='client', scope=CURATION), _op('dg.submit_drain', mutates=True, role='client', scope=CURATION), _op('dg.publish', mutates=True, proof='presence', scope=CURATION), _op('dg.reconcile', mutates=True, proof='presence', scope=CURATION), _op('dg.rebuild', mutates=True, proof='presence', scope=CURATION), _op('dg.settings_validate', mutates=False, scope=CURATION), _op('dg.settings_save', mutates=True, scope=CURATION), _op('dg.collection', mutates=False, role='org', scope=READ), _op('dg.policy', mutates=False, role='org', scope=READ), _op('dg.countersign_status', mutates=False, role='org', scope=READ), _op('dg.audit_chain', mutates=False, role='org', scope=READ), _op('dg.org_review', mutates=False, role='org', scope=READ), _op('dg.keys', mutates=True, proof='operator', role='org', scope=CURATION), _op('dg.work', mutates=True, proof='presence', role='org', scope=CURATION))}
Operation
dataclass
Source code in src/doublegate_sdk/operations.py
allows
True when a transport of scope may emit rpc; unknown verbs are never allowed.
in_scope
doublegate_sdk.inventory
The inventory page as the gate answers it: metadata, scope and coverage, never converted into a claim of admission.
InventoryPage
dataclass
Source code in src/doublegate_sdk/inventory.py
from_response
classmethod
Source code in src/doublegate_sdk/inventory.py
doublegate_sdk.org_rest
The organization gate's two REST doors a fleet client speaks (ADR-0074 d6):
PUT /submissions/{artifact_id} (ADR-0030) and GET /outcomes (ADR-0041).
The body a submission carries is doublegate_sdk.submission's; this module
only moves it and classifies what the door said. Policy — what to do about a
retry, where to keep a cursor — is the caller's.
SubmissionOutcome
dataclass
What the door said about one PUT, in the four words the outbox acts on:
accepted (202/200), retry (503/429 with the door's delay, or any
unexpected status), stopped (401/403: a key problem does not become
correct by waiting), refused (400/413/422: this body, not the key).
Source code in src/doublegate_sdk/org_rest.py
OutcomesPage
dataclass
reason_of
The door's error field, or a short prefix of the body.
Source code in src/doublegate_sdk/org_rest.py
classify_submission_status
Source code in src/doublegate_sdk/org_rest.py
put_submission
put_submission(endpoint: str, artifact_id: str, body: bytes, token: str, *, timeout_s: float = 10.0) -> SubmissionOutcome
One PUT. A transport failure raises GateError('unavailable'); the
caller decides that it is retryable, because that is a policy question.
Source code in src/doublegate_sdk/org_rest.py
pull_outcomes
pull_outcomes(endpoint: str, token: str, *, since: int, limit: int = 500, timeout_s: float = 10.0) -> OutcomesPage
GET /outcomes?since=<cursor>&limit=<n>. auth/scope on 401/403,
remote_error on any other non-200, invalid_response on a non-JSON body.