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
2 changes: 1 addition & 1 deletion dtwo/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dtwo",
"version": "1.2.2",
"version": "1.3.0",
"description": "Manage Dtwo gateways, policies, and Rego with the Dtwo MCP server.",
"author": {
"name": "Dtwo",
Expand Down
2 changes: 1 addition & 1 deletion dtwo/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dtwo",
"version": "1.2.2",
"version": "1.3.0",
"description": "Manage Dtwo gateways, policies, and Rego with the Dtwo MCP server.",
"author": {
"name": "Dtwo",
Expand Down
2 changes: 1 addition & 1 deletion dtwo/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dtwo",
"version": "1.2.2",
"version": "1.3.0",
"description": "Manage Dtwo gateways, policies, and Rego with the Dtwo MCP server.",
"author": {
"name": "Dtwo",
Expand Down
80 changes: 66 additions & 14 deletions dtwo/skills/dtwo-gateway-config/SKILL.md

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions dtwo/skills/dtwo-gateway-config/schema-reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
"schemaDefault": null,
"deployDefault": null,
"target": null,
"description": "Human-gated session clearing (session-control) registration: the IdP app the browser ceremony authenticates against. Arms the platform clear tools and the browser clear ceremony when `intent.enabled: true`, or when `clearing.enabled: true` on a gateway that uses markers without intent capture. A gateway using Dtwo authentication needs no credentials here — it gets its organization's platform-provisioned app — but prefer `clearing: {enabled: true}` over an empty block, so the block states what it does: an empty one arms only while `intent.enabled` is true and parks inert otherwise. `client_id` is only needed for a gateway trusting a customer-run IdP, and `redirect_uri` only for one the browser reaches at a different origin than its token audience. The ceremony issuer is not configured here — it is always `gateway.authentication.jwks_info.jwt_issuer` (the same tenant issues both the inbound tokens and the browser-login ID tokens), and the deploy derives `SESSION_CONTROL_ISSUER` from it."
"description": "Human-gated session clearing (session-control) registration: the IdP app the browser ceremony authenticates against. Arms the platform clear tools and the browser clear ceremony when `intent.enabled: true`, or when `clearing.enabled: true` on a gateway that uses markers without intent capture. A gateway using Dtwo authentication needs no credentials here — it gets its organization's platform-provisioned app — but prefer `clearing: {enabled: true}` over an empty block, so the block states what it does: an empty one arms only while `intent.enabled` is true and parks inert otherwise. `client_id` is only needed for a gateway trusting a customer-run IdP, and `redirect_uri` only for one the browser reaches at a different origin than its token audience, or whose first `jwt_audience` is not an absolute URL (where it is required). The ceremony issuer is not configured here — it is always `gateway.authentication.jwks_info.jwt_issuer` (the same tenant issues both the inbound tokens and the browser-login ID tokens), and the deploy derives `SESSION_CONTROL_ISSUER` from it."
},
{
"name": "log_level",
Expand All @@ -152,7 +152,7 @@
],
"audience": "user",
"schemaDefault": null,
"deployDefault": "DEBUG",
"deployDefault": "INFO",
"target": "LOG_LEVEL",
"description": "ContextForge log level.",
"rationale": "Raise to `DEBUG` or `TRACE` to diagnose a specific issue; lower to `WARNING` or `ERROR` to reduce log volume in production.",
Expand Down Expand Up @@ -704,7 +704,7 @@
{
"path": "gateway.session_control",
"title": "gateway.session_control",
"description": "Human-gated session clearing (session-control) registration: the IdP app the browser ceremony authenticates against. Arms the platform clear tools and the browser clear ceremony when `intent.enabled: true`, or when `clearing.enabled: true` on a gateway that uses markers without intent capture. A gateway using Dtwo authentication needs no credentials here — it gets its organization's platform-provisioned app — but prefer `clearing: {enabled: true}` over an empty block, so the block states what it does: an empty one arms only while `intent.enabled` is true and parks inert otherwise. `client_id` is only needed for a gateway trusting a customer-run IdP, and `redirect_uri` only for one the browser reaches at a different origin than its token audience. The ceremony issuer is not configured here — it is always `gateway.authentication.jwks_info.jwt_issuer` (the same tenant issues both the inbound tokens and the browser-login ID tokens), and the deploy derives `SESSION_CONTROL_ISSUER` from it.",
"description": "Human-gated session clearing (session-control) registration: the IdP app the browser ceremony authenticates against. Arms the platform clear tools and the browser clear ceremony when `intent.enabled: true`, or when `clearing.enabled: true` on a gateway that uses markers without intent capture. A gateway using Dtwo authentication needs no credentials here — it gets its organization's platform-provisioned app — but prefer `clearing: {enabled: true}` over an empty block, so the block states what it does: an empty one arms only while `intent.enabled` is true and parks inert otherwise. `client_id` is only needed for a gateway trusting a customer-run IdP, and `redirect_uri` only for one the browser reaches at a different origin than its token audience, or whose first `jwt_audience` is not an absolute URL (where it is required). The ceremony issuer is not configured here — it is always `gateway.authentication.jwks_info.jwt_issuer` (the same tenant issues both the inbound tokens and the browser-login ID tokens), and the deploy derives `SESSION_CONTROL_ISSUER` from it.",
"fields": [
{
"name": "client_id",
Expand All @@ -730,8 +730,8 @@
"schemaDefault": null,
"deployDefault": null,
"target": "SESSION_CONTROL_REDIRECT_URI",
"description": "Callback URL the browser clear ceremony redirects to, overriding the one the gateway derives from its own token audience. Must be an absolute `https://` URL (`http://` only on localhost, 127.0.0.1, or [::1]). Optional: leave it unset unless the browser reaches the gateway at a different origin than its audience. An explicit value always wins, and the platform allow-lists it verbatim on the IdP application.",
"rationale": "Leave unset in almost all cases — the gateway derives `<origin of the first jwt_audience>/session-control/clear/callback` and the platform registers that. Set it when a reverse proxy or split-horizon DNS puts the browser on a different origin than the audience: give the URL the browser can actually reach, at the root path (`/mcp/*` routes to the streamable-HTTP handler, which rejects browser GETs).",
"description": "Callback URL the browser clear ceremony redirects to, overriding the one the gateway derives from its own token audience. Must be an absolute `https://` URL (`http://` only on localhost, 127.0.0.1, or [::1]). Optional: leave it unset unless the browser reaches the gateway at a different origin than its audience, or the first `jwt_audience` is not an absolute URL. Required while clearing is armed when the first `jwt_audience` is not an absolute URL (there is no origin to derive the callback from). An explicit value always wins, and the platform allow-lists it verbatim on the IdP application.",
"rationale": "Leave unset in almost all cases — the gateway derives `<origin of the first jwt_audience>/session-control/clear/callback` and the platform registers that. Set it when a reverse proxy or split-horizon DNS puts the browser on a different origin than the audience: give the URL the browser can actually reach, at the root path (`/mcp/*` routes to the streamable-HTTP handler, which rejects browser GETs). It is required when the first `jwt_audience` is not an absolute URL — e.g. an Entra ID v2.0 gateway, whose audience is the gateway app's client ID (a GUID) — since there is no origin to derive from, and the deploy rejects an armed gateway that leaves it unset.",
"targetKind": "envVar"
},
{
Expand Down Expand Up @@ -765,6 +765,9 @@
},
{
"message": "While clearing is armed, `jwt_issuer` must be a normalized HTTPS URL (it becomes the ceremony issuer via `SESSION_CONTROL_ISSUER`) and `jwt_audience` must not be blank."
},
{
"message": "While clearing is armed, `redirect_uri` is required when the first entry of `jwt_audience` is not an absolute `http(s)://` URL — for example an Entra ID v2.0 client ID or an `api://` Application ID URI: there is no browser-reachable origin to derive the callback from."
}
]
},
Expand Down
15 changes: 15 additions & 0 deletions dtwo/skills/dtwo-gateway-policy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,20 @@ See **Quick start** above for the create→attach→deploy path and **Creating a
| `dtwo-delete-policy` | Permanently delete a policy by UID. Fails if the policy is still attached to one or more gateways — detach it from every gateway first (see Deleting a Policy). Distinct from `dtwo-revert-policy`, which only restores a prior version |
| `dtwo-list-claims` | Return the union of JWT claim names observed across the tenant, plus the issuers seen. Defaults to tenant-wide; pass `gatewayUid` to scope to a single gateway when the user asks. Call this when authoring or modifying identity-aware policies so rules can reference claims that actually exist; skip for policies that don't read `input.subject.claims`. |

### Catalog Policy Tools

The policy catalog holds ready-made policies a tenant can import. An imported policy remembers which catalog version it came from, so later catalog changes can be offered as updates.

| Tool | Purpose |
|------|---------|
| `dtwo-list-catalog-policies` / `dtwo-get-catalog-policy-filters` / `dtwo-get-catalog-policy` | Browse the catalog, the filters it supports, and one entry |
| `dtwo-import-catalog-policy` | Import a catalog policy as a tenant policy |
| `dtwo-check-policy-updates` | Compare imported policies with the catalog. Each reports `upToDate`, `updateAvailable`, or `removedUpstream`, plus `skipped` when the tenant already dismissed that exact version |
| `dtwo-apply-catalog-policy-update` | Pull the newer catalog version into the policy's **draft**. `applyPolicyCode` and `applyDescription` (both default `true`) choose which parts to replace; the result includes a diff of the Rego. No version is published unless `publish` is set. Call it only after the user has decided how to resolve the update |
| `dtwo-skip-policy-update` | Dismiss an available update until a newer catalog version appears. Doesn't change the policy, and the update can still be applied later |

Never apply a catalog update on your own initiative. Show the user what changed (`dtwo-check-policy-updates`, then the diff), and remember that a pipeline step pinned to a published version keeps running the old Rego until the update is published and the gateway redeployed.

### Marker Registry Tools

Markers are session-state flags that one policy writes and other policies read to gate on (see Managing Markers). These tools are **always registered** on the Dtwo MCP server — they do not depend on any feature flag.
Expand Down Expand Up @@ -366,6 +380,7 @@ After deploying a gateway:
4. Test individual policies in isolation first, then test the full pipeline — ordering of policies in a pipeline can affect the result (e.g., a transform in an earlier step may change data that a later step evaluates)
5. If a policy isn't working as expected:
- **Blanket denies** (all tools blocked) — start with the Rego itself: a `default allow := false` with no rule that matches, or a syntax error anywhere in the bundle, which makes OPA fall back to denying everything. A step's `evalNamespace` differing from the policy's `package` is not a cause: the two are unrelated.
- **See what the gateway decided.** `dtwo-search-recent-events` returns the gateway's OPA policy decisions, errors, and auth events from about the last 30 days. Narrow it by event type, level, or gateway, and call `dtwo-get-event-schema` for the filterable fields. Matching is exact, so filter by fields rather than by words in the message. Use `dtwo-search-events` only for anything older; it is much slower
- For deeper debugging, see the debugging guidance in the companion `dtwo-policy-rego` instructions (debug policies, blanket deny diagnosis)

## End-to-End Example
Expand Down
5 changes: 3 additions & 2 deletions dtwo/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Existing lifecycle tools this skill leans on (documented in the companion skills
|------|---------|
| `dtwo-list-gateways` / `dtwo-get-gateway` | Discover existing gateways; check provisioning/heartbeat state |
| `dtwo-get-gateway-config` / `dtwo-save-gateway-draft-config` / `dtwo-validate-gateway-config` | Read, validate, and save the draft config (MCP servers) — via `dtwo-gateway-config` |
| `dtwo-list-vault-entries` / `dtwo-get-vault-secret-link` | Find an existing Vault entry, or get the Hub link where the user enters an upstream server's credential — via `dtwo-gateway-config` |
| `dtwo-list-catalog-policies` / `dtwo-get-catalog-policy-filters` / `dtwo-import-catalog-policy` | Browse and import starter policies from the policy catalog |
| `dtwo-add-policy` / `dtwo-publish-policy` | Create/publish a custom policy — via `dtwo-gateway-policy` + `dtwo-policy-rego` |
| `dtwo-set-gateway-pipelines` / `dtwo-get-gateway-pipelines` | Attach imported/created policies to the ingress/egress pipelines — via `dtwo-gateway-policy` |
Expand Down Expand Up @@ -297,9 +298,9 @@ MCP servers are the tools the gateway fronts. Start with the fastest path, then

For each one the user picks, add an `mcp_servers` entry with just three fields: `name`, `url` (the table URL), and `transport_type: streamable_http` (the value the Dtwo Hub writes for these same servers; the schema also accepts `streamablehttp`, `http`, and `sse`, and the underscore form and the one-word form reach the gateway identically). Do not add an `authentication` block at all. These are public, anonymous servers, so they need no authentication, and leaving the block off is deliberate: writing `authentication: type: none` tells the gateway to forward your access token to the upstream, which public servers reject and which needlessly exposes your token. Omitting authentication is optional in the schema and validates cleanly. Use a kebab-cased id as the entry `name` (`context7`, `deepwiki`, `microsoft-learn`, `hugging-face`, `cloudflare-docs`, `gitmcp`), matching the Dtwo Hub.

**Custom servers (anything else).** Any MCP server can go behind the gateway; this is the normal path for real usage. Collect its URL and transport from the user. When the upstream needs credentials, add an `authentication` block; the **dtwo-gateway-config** skill owns the supported types (bearer token, basic, custom headers, query parameter, OAuth, certificate) and how to author them, including the secret-placeholder rules. For a public or no-auth upstream, omit the block entirely, the same as the quick-start entries above. One caveat to state plainly: some servers, for example Slack, GitHub, or Jira, assume the user has the right access and need extra configuration on the app side (OAuth apps, API tokens, workspace approval) before the gateway can reach them, so they take a few more steps than the quick-start list.
**Custom servers (anything else).** Any MCP server can go behind the gateway; this is the normal path for real usage. Collect its URL and transport from the user. When the upstream needs credentials, add an `authentication` block; the **dtwo-gateway-config** skill owns the supported types (bearer token, basic, custom headers, query parameter, OAuth, certificate) and how to author them, including how credentials go in. The short version: the config holds a Vault reference such as `<<GITHUB_TOKEN>>`, and the user types the credential itself into the Dtwo Hub through a link you give them, never into the chat. Tell them the gateway can't deploy until that value is saved. For a public or no-auth upstream, omit the block entirely, the same as the quick-start entries above. One caveat to state plainly: some servers, for example Slack, GitHub, or Jira, assume the user has the right access and need extra configuration on the app side (OAuth apps, API tokens, workspace approval) before the gateway can reach them, so they take a few more steps than the quick-start list.

Offer to add more than one server, from either group. Load the **dtwo-gateway-config** skill and follow its flow to edit `mcp_servers`, then `dtwo-save-gateway-draft-config` + `dtwo-validate-gateway-config`. Do not restate the config schema here, that skill owns it (including transport type, outbound auth variants, and secret-placeholder rules). If the user has no server in mind yet, it's fine to save an empty `mcp_servers` and add one later, but tell them the gateway won't front anything until a server is added.
Offer to add more than one server, from either group. Load the **dtwo-gateway-config** skill and follow its flow to edit `mcp_servers`, then `dtwo-save-gateway-draft-config` + `dtwo-validate-gateway-config`. Do not restate the config schema here, that skill owns it (including transport type, outbound auth variants, and Vault references for credentials). If the user has no server in mind yet, it's fine to save an empty `mcp_servers` and add one later, but tell them the gateway won't front anything until a server is added.

**Show progress — checkpoint 3 of 6, do this now.** Redraw the list with **Create your gateway**, **Configure auth**, and **Add MCP servers** checked off. Still to come: Add policies (Phase 10), Connect AI client (Phase 11), Test policies (Phase 12).

Expand Down
4 changes: 2 additions & 2 deletions scripts/generate-schema-digest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -665,9 +665,9 @@ function renderSecretFields(filtered) {
const rows = secrets.map(s => `| \`${s.section}.${s.field}\` |`);

return [
'#### Secret-typed fields — must emit a placeholder, never a literal',
'#### Secret-typed fields: must hold a Vault reference, never a literal',
'',
'Every field marked `secret: true` in the artifact. Emit a self-describing placeholder (`REPLACE_WITH_<FIELD>`, `PLACEHOLDER_<FIELD>`, `YOUR_<FIELD>`, `CHANGE_ME`, or `${ENV_VAR}`); never an inferred or literal credential. Bare `PLACEHOLDER` does not count.',
'Every field marked `secret: true` in the artifact. Put a Vault reference there (`<<NAME>>`, with no `|` fallback), never an inferred or literal credential. See Secrets and the Vault for how the user adds the value.',
'',
'| Path |',
'|---|',
Expand Down
2 changes: 1 addition & 1 deletion skill-harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ failing check marks the sample as failed; the per-prompt pass-rate is
| `no_hallucinated_keys` | Every path in the YAML resolves to a real field in `schema-reference.json` (record-leaf children accepted when they match the leaf's declared key pattern). |
| `no_dropped_keys` | Round-trip path-set subtraction: `paths(input) \ paths(parsed)` must be empty. |
| `safe_defaults_preserved` | A small curated seed list of safety-relevant defaults must not be silently weakened. The list has two halves: `SAFE_DEFAULT_SEEDS`, whose safe values are derived from the artifact's `schemaDefault` / `deployDefault`, and `GATEWAY_OWNED_SAFE_DEFAULTS`, a short explicit list for values the gateway applies at boot — hand-written as the harness's own opinion and cross-checked against the `gatewayDefault` the artifact declares for them. Fixtures opt out per-path with `expect.safe_default_opt_out`. |
| `secrets_are_placeholders` | Every `secret: true` leaf is absent or holds a placeholder matching `/^<?(REPLACE_\|PLACEHOLDER_\|CHANGE_?ME\|YOUR[_-]\|your[_-])\|^\$\{/`. |
| `secrets_are_placeholders` | Every `secret: true` leaf is absent or holds either a Vault reference that is the whole value (`<<NAME>>`, no `\|` fallback), which is what the skill writes into a config saved to a gateway, or a placeholder matching `/^<?(REPLACE_\|PLACEHOLDER_\|CHANGE_?ME\|YOUR[_-]\|your[_-])\|^\$\{/`. |

Plus per-fixture `required_paths`, `forbidden_paths`, and
`value_constraints` (regex / equals / min_length).
Expand Down
Loading
Loading