Files
git.stella-ops.org/docs/api/graph-gateway-spec-draft.yaml
StellaOps Bot 7c39058386
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Symbols Server CI / symbols-smoke (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
up
2025-11-24 20:57:49 +02:00

588 lines
19 KiB
YAML

openapi: 3.0.3
info:
title: StellaOps Graph Gateway (draft)
version: 0.0.3-pre
description: |
Draft API surface for graph search/query/paths/diff/export with streaming tiles,
cost budgets, overlays, and RBAC headers. Aligns with sprint 0207 Wave 1 outline
(GRAPH-API-28-001..011).
servers:
- url: https://gateway.local/api
security:
- bearerAuth: []
paths:
/graph/search:
post:
summary: Search graph nodes with prefix/exact semantics and filters
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/TenantHeader'
- $ref: '#/components/parameters/RequestIdHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SearchRequest'
responses:
'200':
description: Stream of search tiles (NDJSON)
content:
application/x-ndjson:
schema:
$ref: '#/components/schemas/TileEnvelope'
examples:
sample:
summary: Node + cursor tiles
value: |
{"type":"node","seq":0,"data":{"id":"gn:tenant:component:abc","kind":"component","tenant":"acme","attributes":{"purl":"pkg:npm/lodash@4.17.21"}},"cost":{"limit":1000,"remaining":999,"consumed":1}}
{"type":"cursor","seq":1,"data":{"token":"cursor-123","resumeUrl":"https://gateway.local/api/graph/query?cursor=cursor-123"}}
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/BudgetExceeded' }
responses:
'200':
description: Stream of search tiles (NDJSON)
content:
application/x-ndjson:
schema:
$ref: '#/components/schemas/TileEnvelope'
examples:
sample:
summary: Node + cursor tiles
value: |
{"type":"node","seq":0,"data":{"id":"gn:tenant:component:abc","kind":"component","tenant":"acme","attributes":{"purl":"pkg:npm/lodash@4.17.21"}},"cost":{"limit":1000,"remaining":999,"consumed":1}}
{"type":"cursor","seq":1,"data":{"token":"cursor-123","resumeUrl":"https://gateway.local/api/graph/search?cursor=cursor-123"}}
headers:
X-RateLimit-Remaining:
description: Remaining request budget within the window.
schema:
type: integer
Retry-After:
description: Seconds until next request is allowed when rate limited.
schema:
type: integer
/graph/query:
post:
summary: Execute graph query with budgeted streaming tiles
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/TenantHeader'
- $ref: '#/components/parameters/RequestIdHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/QueryRequest'
responses:
'200':
description: Stream of query tiles (NDJSON)
content:
application/x-ndjson:
schema:
$ref: '#/components/schemas/TileEnvelope'
examples:
mixedTiles:
summary: Node + edge + stats tiles
value: |
{"type":"node","seq":0,"data":{"id":"gn:tenant:artifact:sha256:...","tenant":"acme","kind":"artifact","attributes":{"sbom_digest":"sha256:abc"}}}
{"type":"edge","seq":1,"data":{"id":"ge:tenant:CONTAINS:...","sourceId":"gn:tenant:artifact:...","targetId":"gn:tenant:component:...","kind":"CONTAINS"}}
{"type":"stats","seq":2,"data":{"nodesEmitted":1,"edgesEmitted":1,"depthReached":2,"cacheHitRatio":0.8}}
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/BudgetExceeded' }
responses:
'200':
description: Stream of query tiles (NDJSON)
content:
application/x-ndjson:
schema:
$ref: '#/components/schemas/TileEnvelope'
examples:
mixedTiles:
summary: Node + edge + stats tiles
value: |
{"type":"node","seq":0,"data":{"id":"gn:tenant:artifact:sha256:...","tenant":"acme","kind":"artifact","attributes":{"sbom_digest":"sha256:abc"}}}
{"type":"edge","seq":1,"data":{"id":"ge:tenant:CONTAINS:...","sourceId":"gn:tenant:artifact:...","targetId":"gn:tenant:component:...","kind":"CONTAINS"}}
{"type":"stats","seq":2,"data":{"nodesEmitted":1,"edgesEmitted":1,"depthReached":2,"cacheHitRatio":0.8}}
headers:
X-RateLimit-Remaining:
description: Remaining request budget within the window.
schema:
type: integer
Retry-After:
description: Seconds until next request is allowed when rate limited.
schema:
type: integer
/graph/paths:
post:
summary: Find constrained paths between node sets (depth ≤ 6)
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/TenantHeader'
- $ref: '#/components/parameters/RequestIdHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PathsRequest'
responses:
'200':
description: Stream of path tiles ordered by hop
content:
application/x-ndjson:
schema:
$ref: '#/components/schemas/TileEnvelope'
examples:
pathTiles:
summary: Path tiles grouped by hop
value: |
{"type":"node","seq":0,"data":{"id":"gn:tenant:component:src","kind":"component","tenant":"acme"}}
{"type":"edge","seq":1,"data":{"id":"ge:tenant:DEPENDS_ON:1","sourceId":"gn:tenant:component:src","targetId":"gn:tenant:component:dst","kind":"DEPENDS_ON"}}
{"type":"stats","seq":2,"data":{"nodesEmitted":2,"edgesEmitted":1,"depthReached":1}}
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/BudgetExceeded' }
responses:
'200':
description: Stream of path tiles ordered by hop
content:
application/x-ndjson:
schema:
$ref: '#/components/schemas/TileEnvelope'
examples:
pathTiles:
summary: Path tiles grouped by hop
value: |
{"type":"node","seq":0,"data":{"id":"gn:tenant:component:src","kind":"component","tenant":"acme","attributes":{"purl":"pkg:npm/demo@1.0.0"},"pathHop":0}}
{"type":"edge","seq":1,"data":{"id":"ge:tenant:DEPENDS_ON:1","sourceId":"gn:tenant:component:src","targetId":"gn:tenant:component:dst","kind":"DEPENDS_ON","pathHop":1}}
{"type":"stats","seq":2,"data":{"nodesEmitted":2,"edgesEmitted":1,"depthReached":1}}
/graph/diff:
post:
summary: Stream diff between two graph snapshots with overlay deltas
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/TenantHeader'
- $ref: '#/components/parameters/RequestIdHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DiffRequest'
responses:
'200':
description: Stream of diff tiles (added/removed/changed)
content:
application/x-ndjson:
schema:
$ref: '#/components/schemas/TileEnvelope'
examples:
diffTiles:
summary: Added/removed tiles
value: |
{"type":"node","seq":0,"data":{"id":"gn:tenant:component:new","kind":"component","tenant":"acme","attributes":{"purl":"pkg:npm/new@1.0.0"}}}
{"type":"diagnostic","seq":1,"data":{"level":"info","message":"snapshot diff complete"}}
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/BudgetExceeded' }
/graph/export/{jobId}/manifest:
get:
summary: Download deterministic checksum manifest for a completed export job
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/TenantHeader'
- $ref: '#/components/parameters/RequestIdHeader'
- name: jobId
in: path
required: true
schema:
type: string
responses:
'200':
description: Deterministic manifest
content:
application/json:
schema:
type: object
properties:
files:
type: array
items:
type: object
properties:
path: { type: string }
sha256: { type: string }
size: { type: integer }
exportId: { type: string }
'404': { description: Manifest not ready or job missing }
/graph/export:
post:
summary: Request export job for snapshot or query result
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/TenantHeader'
- $ref: '#/components/parameters/RequestIdHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ExportRequest'
responses:
'202':
description: Export job accepted
content:
application/json:
schema:
$ref: '#/components/schemas/ExportJob'
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
/graph/export/{jobId}:
get:
summary: Check export job status or download manifest
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/TenantHeader'
- $ref: '#/components/parameters/RequestIdHeader'
- name: jobId
in: path
required: true
schema:
type: string
responses:
'200':
description: Job status
content:
application/json:
schema:
$ref: '#/components/schemas/ExportJob'
'404':
description: Job not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
TenantHeader:
name: X-Stella-Tenant
in: header
required: true
schema:
type: string
description: Tenant identifier enforced on all routes.
RequestIdHeader:
name: X-Request-Id
in: header
required: false
schema:
type: string
description: Optional caller-provided correlation id, echoed in responses.
schemas:
OverlayPayload:
type: object
description: Overlay content injected into node/edge tiles when requested (policy/vex/advisory).
properties:
kind:
type: string
enum: [policy, vex, advisory]
version:
type: string
description: Contract version of the overlay payload.
data:
type: object
additionalProperties: true
required: [kind, version, data]
CostBudget:
type: object
properties:
limit:
type: integer
minimum: 1
example: 1000
remaining:
type: integer
minimum: 0
example: 995
consumed:
type: integer
minimum: 0
example: 5
required: [limit, remaining, consumed]
TileEnvelope:
type: object
properties:
type:
type: string
enum: [node, edge, stats, cursor, diagnostic]
seq:
type: integer
minimum: 0
example: 0
cost:
$ref: '#/components/schemas/CostBudget'
data:
description: Payload varies by tile type.
oneOf:
- $ref: '#/components/schemas/NodeTile'
- $ref: '#/components/schemas/EdgeTile'
- $ref: '#/components/schemas/StatsTile'
- $ref: '#/components/schemas/CursorTile'
- $ref: '#/components/schemas/DiagnosticTile'
required: [type, seq]
NodeTile:
type: object
properties:
id: { type: string }
kind: { type: string }
tenant: { type: string }
attributes: { type: object }
pathHop:
type: integer
description: Hop depth for path streaming responses.
overlays:
type: object
description: Optional overlay payloads (policy/vex/advisory) keyed by overlay kind.
additionalProperties:
$ref: '#/components/schemas/OverlayPayload'
required: [id, kind, tenant]
EdgeTile:
type: object
properties:
id: { type: string }
kind: { type: string }
sourceId: { type: string }
targetId: { type: string }
tenant: { type: string }
attributes: { type: object }
pathHop:
type: integer
description: Hop depth for path streaming responses.
overlays:
type: object
additionalProperties:
$ref: '#/components/schemas/OverlayPayload'
required: [id, kind, sourceId, targetId, tenant]
StatsTile:
type: object
properties:
nodesEmitted: { type: integer, minimum: 0 }
edgesEmitted: { type: integer, minimum: 0 }
depthReached: { type: integer, minimum: 0 }
cacheHitRatio: { type: number, minimum: 0, maximum: 1 }
required: [nodesEmitted, edgesEmitted]
CursorTile:
type: object
properties:
token: { type: string }
resumeUrl: { type: string, format: uri }
required: [token]
DiagnosticTile:
type: object
properties:
level: { type: string, enum: [info, warn, error] }
message: { type: string }
details: { type: object }
required: [level, message]
SearchRequest:
type: object
properties:
query:
type: string
description: Prefix or exact text; required unless filters present.
kinds:
type: array
items:
type: string
limit:
type: integer
default: 50
maximum: 500
filters:
type: object
additionalProperties: true
ordering:
type: string
enum: [relevance, id]
cursor:
type: string
description: Resume token from prior search response.
required: [kinds]
QueryRequest:
type: object
properties:
dsl:
type: string
description: DSL expression for graph traversal (mutually exclusive with filter).
filter:
type: object
description: Structured filter alternative to DSL.
budget:
type: object
properties:
nodeCap: { type: integer }
edgeCap: { type: integer }
timeMs: { type: integer }
overlays:
type: array
items:
type: string
enum: [policy, vex, advisory]
explain:
type: string
enum: [none, minimal, full]
default: none
cursor:
type: string
description: Resume token from prior query response.
anyOf:
- required: [dsl]
- required: [filter]
PathsRequest:
type: object
properties:
sourceIds:
type: array
items: { type: string }
minItems: 1
targetIds:
type: array
items: { type: string }
minItems: 1
maxDepth:
type: integer
maximum: 6
default: 4
constraints:
type: object
properties:
edgeKinds:
type: array
items: { type: string }
fanoutCap:
type: integer
overlays:
type: array
items: { type: string }
required: [sourceIds, targetIds]
DiffRequest:
type: object
properties:
snapshotA: { type: string }
snapshotB: { type: string }
filters:
type: object
additionalProperties: true
cursor:
type: string
description: Resume token from prior diff stream.
required: [snapshotA, snapshotB]
ExportRequest:
type: object
properties:
snapshotId:
type: string
queryRef:
type: string
formats:
type: array
items:
type: string
enum: [graphml, csv, ndjson, png, svg]
includeOverlays:
type: boolean
default: false
anyOf:
- required: [snapshotId]
- required: [queryRef]
required: [formats]
ExportJob:
type: object
properties:
jobId: { type: string }
status: { type: string, enum: [pending, running, succeeded, failed] }
checksumManifestUrl: { type: string, format: uri }
downloadUrl: { type: string, format: uri }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
message: { type: string }
expiresAt: { type: string, format: date-time, description: "Optional expiry for download links." }
required: [jobId, status]
Error:
type: object
properties:
error:
type: string
enum: [GRAPH_BUDGET_EXCEEDED, GRAPH_VALIDATION_FAILED, GRAPH_RATE_LIMITED, GRAPH_UNAUTHORIZED]
message:
type: string
details:
type: object
request_id:
type: string
required: [error, message]
responses:
ValidationError:
description: Request failed validation
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Missing or invalid credentials
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BudgetExceeded:
description: Budget exhausted mid-stream; includes partial cursor details
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
headers:
Retry-After:
description: Seconds until budgets refresh.
schema:
type: integer