Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/fluid-vendor-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# The vendored FLUID schemas under vendor/fluid/ must match what FLUID publishes.
#
# FLUX does not own the FLUID schema; it references it across the seam. The copies
# in vendor/fluid/ exist so the validator can resolve a referenced contract offline,
# which means they are a cache — and an unguarded cache drifts.
#
# They are in sync today. Nothing enforced that, which is the actual gap: a claim
# about another project's artefact is only true if the CI of the side making the
# claim proves it. That is the rule this repository now applies to itself after
# shipping a false "field-to-field aligned with FLUID" statement through several
# releases (see docs/reconciliation.md).
#
# Comparison is on canonical JSON (sorted keys, no whitespace) so cosmetic
# formatting differences do not fail the build — only a real content divergence does.

name: fluid-vendor-sync

on:
push:
branches: [main]
paths:
- "vendor/fluid/**"
- ".github/workflows/fluid-vendor-sync.yml"
pull_request:
paths:
- "vendor/fluid/**"
- ".github/workflows/fluid-vendor-sync.yml"
schedule:
# Weekly: FLUID can publish a new patch without FLUX touching anything, and a
# cache that silently ages is exactly what this guards against.
- cron: "0 6 * * 1"
workflow_dispatch:

permissions:
contents: read

env:
FLUID_RAW_BASE: https://raw.githubusercontent.com/open-data-protocol/fluid/main/schema

jobs:
vendor-sync:
name: vendored FLUID schemas match upstream
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: compare each vendored schema against the published one
run: |
set -euo pipefail
status=0
for path in vendor/fluid/fluid-schema-*.json; do
name="$(basename "$path")"
if ! curl -fsSL "${FLUID_RAW_BASE}/${name}" -o "/tmp/${name}"; then
echo "::error::${name} is vendored here but not published upstream."
echo "::error::Either it was withdrawn, or this copy was never a real FLUID release."
status=1
continue
fi
if python3 - "$path" "/tmp/${name}" <<'PY'
import json, sys
canon = lambda p: json.dumps(json.load(open(p)), sort_keys=True, separators=(",", ":"))
sys.exit(0 if canon(sys.argv[1]) == canon(sys.argv[2]) else 1)
PY
then
echo "✅ ${name} matches upstream"
else
echo "::error::${name} has drifted from the published FLUID schema."
echo "::error::Re-vendor it: curl -fsSL ${FLUID_RAW_BASE}/${name} -o ${path}"
echo "::error::then run: python3 scripts/sync-package-data.py"
diff <(python3 -m json.tool --sort-keys "$path") \
<(python3 -m json.tool --sort-keys "/tmp/${name}") | head -40 || true
status=1
fi
done
exit $status
73 changes: 65 additions & 8 deletions docs/reconciliation.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,74 @@ matching FLUID's conventions.

### Governed — delivered in schema, cross-checks in the validator
- `agentPolicy` (root-level, all kinds): model allow-list, token budget,
use-case limits — vocabulary aligned with FLUID `exposes[].policy.agentPolicy`.
use-case limits. **Not** field-for-field with FLUID — see "The agentPolicy
divergence" below.
- `skills` (root-level, all kinds): the uniform agentic extension point of
paper §3.5. Declaring `skills` requires `agentPolicy` (schema-enforced).
- `sovereignty` on `World` and `Simulation` (`allowedZones`, `enforcement: block|audit`).
- `ConsentProfile` can no longer be empty, and its vocabulary
(`allowedUseCases`/`deniedUseCases`) matches FLUID's, so seam-crossing policy
comparison is field-to-field. The "contract must be at least as strict as the
gating ConsentProfile" rule is enforced by the reference validator at the
seam: an emitted expose's `policy.agentPolicy` may not allow a use case a
gating profile denies, nor exceed its allow-list (`scripts/validate.py`,
regression-tested).
- `ConsentProfile` can no longer be empty. The "contract must be at least as
strict as the gating ConsentProfile" rule is enforced by the reference
validator at the seam: an emitted expose's `policy.agentPolicy` may not allow
a use case a gating profile denies, nor exceed its allow-list
(`scripts/validate.py`, regression-tested). **That enforcement is real, but
its reach is narrower than this document previously claimed** — see below.


## The agentPolicy divergence

This document previously said FLUX's `agentPolicy` vocabulary was "aligned with
FLUID's" and that "seam-crossing policy comparison is field-to-field". Both
statements were wrong, and the schema carried the same claim in its own
`$defs/agentPolicy` description. They are corrected here rather than quietly
dropped, because a false alignment claim is worse than a documented gap: it
invites implementers to build a comparison that cannot work.

Measured against `fluid-schema-0.7.5.json`:

| | FLUX `agentPolicy` | FLUID `exposes[].policy.agentPolicy` |
|---|---|---|
| members | 5 | 13 |
| token budget | `tokenBudget` (integer) | `maxTokensPerRequest` + `maxTokensPerDay` |
| `purposeLimitation` | boolean | string |
| use-case values | free strings (`minLength: 1`) | 12-value enum |
| FLUID-only | — | `deniedModels`, `canReason`, `canStore`, `retentionPolicy`, `auditRequired`, `tags`, `labels` |

Three members line up by name *and* type: `allowedModels`, `allowedUseCases`,
`deniedUseCases`.

### What this costs at the seam

The use-case row is the one that bites. FLUX accepts any non-empty string;
FLUID accepts twelve specific values. So a `ConsentProfile` can deny a use case
that no FLUID contract is able to express, and the "at least as strict" check
then has nothing to bind against on the FLUID side.

This is not hypothetical. Of the eleven distinct use-case values appearing in
FLUX's own 34 enforcement vectors — `advertising`, `persona_variation`,
`journey_synthesis`, `pricing_optimisation`, `exfiltrate-pii` and the rest —
**none** can be written into a FLUID contract. The seam comparison the previous
wording described has never had a working example in this repository's test
suite.

What the validator does enforce is still worth having: within FLUX, a
simulation's declared policy is checked against its gating profile, and that is
regression-tested. What it cannot do is carry a FLUX use-case restriction
across the seam into a FLUID contract.

### Resolving it

Two options, and this is an open decision, not a plan:

1. **FLUX rebases onto FLUID's vocabulary** — `$ref` FLUID's published
`agentPolicy` `$id` instead of restating it. Cleanest, and it makes the seam
claim true. It is a breaking change to FLUX (`tokenBudget` splits in two,
`purposeLimitation` changes type, use-case values become constrained) and the
34 vectors would need rebasing. Cheapest while FLUX has no external
implementers.
2. **FLUX keeps its own vocabulary** and this table stands as the permanent,
honest statement of what does and does not cross the seam.

Either way the rule holds: no alignment claim without a CI gate that can fail.

## The four seams (paper §3.4)

Expand Down
2 changes: 1 addition & 1 deletion flux_spec/data/schema/flux-schema-0.5.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,7 @@
"default": true
}
},
"description": "Vocabulary aligned with FLUID's exposes[].policy.agentPolicy so seam-crossing policy comparison is field-to-field."
"description": "FLUX's own agent-policy vocabulary. It is NOT field-for-field identical to FLUID's exposes[].policy.agentPolicy and must not be compared as if it were: FLUX declares 5 members to FLUID's 13, FLUX's tokenBudget has no FLUID counterpart (FLUID splits it into maxTokensPerRequest and maxTokensPerDay), purposeLimitation is boolean here and a string in FLUID, and FLUX's use-case values are free strings where FLUID constrains them to a 12-value enum. See docs/reconciliation.md for the divergence table and what the seam validator can and cannot enforce because of it."
},
"skillBinding": {
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion flux_spec/data/schema/flux-schema-latest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,7 @@
"default": true
}
},
"description": "Vocabulary aligned with FLUID's exposes[].policy.agentPolicy so seam-crossing policy comparison is field-to-field."
"description": "FLUX's own agent-policy vocabulary. It is NOT field-for-field identical to FLUID's exposes[].policy.agentPolicy and must not be compared as if it were: FLUX declares 5 members to FLUID's 13, FLUX's tokenBudget has no FLUID counterpart (FLUID splits it into maxTokensPerRequest and maxTokensPerDay), purposeLimitation is boolean here and a string in FLUID, and FLUX's use-case values are free strings where FLUID constrains them to a 12-value enum. See docs/reconciliation.md for the divergence table and what the seam validator can and cannot enforce because of it."
},
"skillBinding": {
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion schema/flux-schema-0.5.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,7 @@
"default": true
}
},
"description": "Vocabulary aligned with FLUID's exposes[].policy.agentPolicy so seam-crossing policy comparison is field-to-field."
"description": "FLUX's own agent-policy vocabulary. It is NOT field-for-field identical to FLUID's exposes[].policy.agentPolicy and must not be compared as if it were: FLUX declares 5 members to FLUID's 13, FLUX's tokenBudget has no FLUID counterpart (FLUID splits it into maxTokensPerRequest and maxTokensPerDay), purposeLimitation is boolean here and a string in FLUID, and FLUX's use-case values are free strings where FLUID constrains them to a 12-value enum. See docs/reconciliation.md for the divergence table and what the seam validator can and cannot enforce because of it."
},
"skillBinding": {
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion schema/flux-schema-latest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,7 @@
"default": true
}
},
"description": "Vocabulary aligned with FLUID's exposes[].policy.agentPolicy so seam-crossing policy comparison is field-to-field."
"description": "FLUX's own agent-policy vocabulary. It is NOT field-for-field identical to FLUID's exposes[].policy.agentPolicy and must not be compared as if it were: FLUX declares 5 members to FLUID's 13, FLUX's tokenBudget has no FLUID counterpart (FLUID splits it into maxTokensPerRequest and maxTokensPerDay), purposeLimitation is boolean here and a string in FLUID, and FLUX's use-case values are free strings where FLUID constrains them to a 12-value enum. See docs/reconciliation.md for the divergence table and what the seam validator can and cannot enforce because of it."
},
"skillBinding": {
"type": "object",
Expand Down
Loading