OpenWOP openwop.dev
FieldValue
RFC0076
TitlePack runtime-requirements declaration + host-provided safe-fetch
StatusAccepted
Track status§A (runtime.requires[]) Accepted 2026-05-29 — verified live on a non-steward host (MyndHyve); §B (ctx.http.safeFetch under httpClient.safeFetch) Accepted 2026-05-29 — durable-emission MUST proven live on a non-steward host (MyndHyve workflow-runtime-00412-xow). Both tracks Accepted ⇒ RFC 0076 Accepted overall.
Author(s)openwop working group (steward), prompted by the MyndHyve RFC 0072 §B second-host debrief (2026-05-28)
Created2026-05-28
Updated2026-05-29 (Draft → Active — steward acceptance, comment window waived per GOVERNANCE.md single-maintainer lazy consensus; wire shapes now locked. §A wire surface + conformance landed. rev 4 (2026-05-29) — §B wire surface landed: safeFetch reconciled under the existing httpClient capability (httpClient.safeFetch, reusing the http-client-ssrf-guard invariant + maxResponseBodyBytes cap) rather than a standalone host.http.safeFetch; §host.http prose + safefetch-behavior.test.ts + the POST /v1/host/sample/http/safe-fetch seam; §B reference-impl deferred. rev 3 — MyndHyve §A schema-diff review folded in: elided additive diff synced to live $defs/Runtime, oneOf/const/description vocabulary (no JSON comments), empty-array≡omission, coarser-parent-breadth + closed-enum versioning rules, peerDependencies-compose note, Q5 credential-provenance breadcrumb). Rev 2 (2026-05-28) — second-host comment-window review: Q1–Q4 resolved, env.read added, §B audit MUST + request-init clamps + RFC 0069 composition + §A/§B sequencing. §B graduated Active → Accepted 2026-05-29: the durable-emission MUST (§Conformance, safefetch-live-audit.test.ts — PR #336) was proven on a non-steward host. MyndHyve workflow-runtime advertises httpClient.safeFetch.supported: true + ssrfGuard: true + toolHooks.prePostEvents: true, and the production ctx.http.safeFetch path (exercised via the POST /v1/host/sample/http/safe-fetch-run open seam on rev 00412-xow) persists the agent.toolCalled/agent.toolReturned pair to the durable run-event log with transport: 'http' + the RFC 0002 §B causationId chain (toolReturned.causationId === toolCalled.eventId). Steward-verified by independent curl 2026-05-29 (fetched-path run → durable pair + chain; the status:'error'/fetch_failed is Cloud Run sandbox egress, which the scenario correctly does not gate on — the SSRF guard _passed_ the public URL, yielding outcome:'fetched'). With §A already Accepted, RFC 0076 is Accepted overall.
Affectsschemas/node-pack-manifest.schema.json (new optional runtime.requires[]), schemas/capabilities.schema.json (httpClient block + httpClient.safeFetch), spec/v1/node-packs.md, spec/v1/registry-operations.md (install-time gate), spec/v1/host-capabilities.md (§host.http — ctx.http.safeFetch), spec/v1/host-sample-test-seams.md (two seams), conformance/src/scenarios/* (runtime-requires + safefetch)
Compatibilityadditive per COMPATIBILITY.md
Supersedes
Superseded by

Summary

A pack's runtime code may need platform primitives the host's sandbox denies by default — core.openwop.http@1.1.2 reaches for node:dns/promises to do SSRF defense before every fetch, which a tight default-deny sandbox refuses, so the host discovers the requirement only by a failed _trial-load_. This RFC adds two additive surfaces: (1) an abstract, runtime-agnostic runtime.requires[] declaration on the pack manifest so a host can gate at install time instead of at first invocation; and (2) an OPTIONAL host-provided ctx.http.safeFetch(url, init?) that centralizes outbound-request SSRF defense in the host, so packs no longer reach for node:dns/promises themselves. Both are opt-in and backward-compatible: packs without runtime.requires and hosts without safeFetch behave exactly as today.

Motivation

Today, pack-runtime platform requirements are not first-class. The manifest has three adjacent-but-distinct mechanisms, none of which expresses "this pack's code calls dns.lookup / opens outbound sockets":

  • runtime (schemas/node-pack-manifest.schema.jsonlanguage/entry) — _which_ runtime, not what it touches.
  • peerDependencies (RFC 0072 §C) — host agent-runtime capability tiers (agents.manifestRuntime, host.agentRuntime).
  • NodeModule.requires + capabilities.runtimeCapabilities (capabilities.md §"Runtime capabilities") — host-advertised opaque facilities consumed _per node at dispatch_ (chat.sendPrompt, canvas.write). registry-operations.md §"Host-private marketplace" already shows requires: ['<host>.canvas.write'] gating load.

What's missing is the platform/sandbox axis: the primitives the pack's _own code_ exercises (DNS resolution, outbound fetch, crypto, subprocess, filesystem). A sandbox-based host (the realistic deployment for untrusted community. / vendor. packs) runs a default deny-list and only learns a pack needs node:dns when the pack throws at first invocation. MyndHyve had to carve node:dns/promises out of its sandbox by trial-and-error to load core.openwop.http at all (RFC 0072 §B debrief, finding 1).

The spec is the right place because pack portability is a cross-host guarantee: a pack that loads on host A's sandbox but silently fails on host B's tighter sandbox is exactly the "portable manifest, host-specific runtime" failure RFC 0072 set out to close. An install-time, declarative gate makes the boundary inspectable before a run depends on it.

Separately, finding 1(b): packs that perform SSRF defense by direct DNS (the core.openwop.http assertPublicUrl pattern) each re-implement the metadata-endpoint blocklist and private-range checks, and each must reach for node:dns. Centralizing the defense in a host-provided safeFetch is more secure (one audited, host-maintained blocklist; audit-loggable via RFC 0064 agent.toolCalled), more portable (no node:dns dependency in the pack), and consistent across packs — worth settling before more packs follow core.openwop.http's lead.

Proposal

§A — runtime.requires[] on the pack manifest (additive)

Add an OPTIONAL requires array to the manifest's $defs/Runtime object, drawn from a controlled, runtime-agnostic vocabulary of platform primitives — _not_ raw Node builtin names (node:dns/promises does not translate to the Python / Go / wasm runtimes the spec already supports). The diff below is additive-only — the + lines are the entire change; the surrounding properties (elided with ) are unchanged, including language (which already carries wasm-component, RFC 0008.1), format, and minRuntimeVersion. JSON Schema is strict JSON, so per-value semantics ride in oneOf/const/description rather than // comments:

   "Runtime": {
     "type": "object",
     "required": ["language", "entry"],
     "properties": {
       "language":          { "…": "unchanged (incl. wasm-component, RFC 0008.1)" },
       "entry":             { "…": "unchanged" },
       "format":            { "…": "unchanged" },
       "minRuntimeVersion": { "…": "unchanged" },
+      "requires": {
+        "type": "array",
+        "uniqueItems": true,
+        "description": "RFC 0076. Abstract platform primitives the pack's runtime code exercises, for install-time sandbox gating. Runtime-agnostic (not language builtin names). Absent or [] ⇒ no elevated platform needs.",
+        "items": {
+          "oneOf": [
+            { "const": "net.dns",      "description": "Resolves hostnames (e.g., SSRF pre-flight)." },
+            { "const": "net.outbound", "description": "Opens outbound network connections / fetch." },
+            { "const": "crypto",       "description": "Primitives beyond the standard hashing the host already provides." },
+            { "const": "subprocess",   "description": "Spawns a child process (composes with the RFC 0069 exec-class contract when the host advertises it)." },
+            { "const": "fs.read",      "description": "Reads the local filesystem." },
+            { "const": "fs.write",     "description": "Writes the local filesystem." },
+            { "const": "env.read",     "description": "Reads the process environment (may expose deployment secrets if the host does not scrub it)." },
+            { "const": "clock",        "description": "Reads wall-clock time as a behavioral input — gated for REPLAY determinism, not access control. A pack that branches on the clock is non-deterministic on replay (replay.md)." }
+          ]
+        }
+      }
     },
     "additionalProperties": false
   }

clock and env.read are the two non-obvious members. clock is included not because wall-clock access is privileged (it is a language global) but because a pack that branches on it is non-deterministic on replay (replay.md) — declaring it lets a replay-strict host gate or instrument it; future maintainers should not trim it as redundant with Date.now(). env.read is gated because an unscrubbed process.env can leak deployment secrets into pack code.

Behavior (normative).

  • A pack MAY declare runtime.requires[]. Absent — or an empty array (runtime.requires: []) — ⇒ the pack asserts no elevated platform needs (today's behavior); the two are equivalent, and a host MUST NOT read a distinct meaning into the empty array.
  • A host that gates platform access (a sandbox host) MUST evaluate runtime.requires[] at install time: every listed primitive its sandbox can grant ⇒ install; any primitive it will not grant ⇒ the host MUST refuse install with pack_runtime_requirement_unmet naming the unmet primitive(s) — the install-time analogue of the dispatch-time capability_not_provided (capabilities.md). It MUST NOT silently install and fail at first invocation.
  • A host that does not gate platform access (grants the runtime's full standard library) MAY ignore the field for _enforcement_ — every primitive is already available; there is nothing to refuse. It SHOULD nevertheless project runtime.requires[] onto the pack's inventory/summary entry (e.g. the pack-summary projection on the GET /v1/agents / pack listing) so operators retain visibility into the declared platform footprint at install time. This forward-compatibly prepares the workspace approval ledger for a later migration to a sandbox-enforcing host (which packs to re-evaluate). It carries no execution requirement.
  • runtime.requires[] is a declaration of intent for gating, not an authorization grant. It does not widen what a pack may do; it lets the host decide _before_ load whether it is willing to grant what the pack will attempt. A host MUST still enforce its sandbox at runtime — a pack that declares net.dns but attempts subprocess is still denied the undeclared primitive.

Vocabulary versioning (normative). The vocabulary extends additively (minor version; the wasm-* enum-addition precedent). Two rules keep the install gate sound as it refines:

  • Coarser-parent breadth. When a later revision introduces a finer-grained primitive (e.g. net.outbound.http vs net.outbound.raw), a host MUST treat the coarser parent (net.outbound) as at-least-as-broad — a manifest declaring the coarser term implicitly requires every finer sub-token the host might split it into. This prevents a vocabulary refinement from becoming a covert security regression: a host that newly enforces a finer distinction cannot silently accept a coarser declaration it would refuse at the finer grain.
  • Closed-enum / version-pinned (the safety contract). Schemas are version-pinned per host. A host validating manifests against schema vN MUST refuse a vocabulary token introduced after vN with invalid_manifest. This is intentional, not a gap: an old host refusing a newer requirement it does not understand is the install-time safety contract — a host MUST NOT grant a primitive it has not yet specified.

Examples.

_Positive._ core.openwop.http declares "requires": ["net.dns", "net.outbound"]. A sandbox host that permits outbound HTTP + DNS installs it; the requirement is visible on the pack's discovery page; no trial-load.

_Negative (refused install)._ A pack declares "requires": ["subprocess"]. A host whose sandbox forbids child processes refuses install with pack_runtime_requirement_unmet { unmet: ["subprocess"] } — the operator sees the boundary at install, not a production run failure.

_Negative (validation)._ "requires": ["node:dns/promises"] fails manifest validation (400 invalid_manifest) — raw builtin names are not in the controlled vocabulary; the abstract net.dns is the portable equivalent.

Error payload (normative). pack_runtime_requirement_unmet reuses the capability_not_provided envelope shape (capabilities.md):

{
  "error": "pack_runtime_requirement_unmet",
  "unmet": ["subprocess"],
  "manifest": "core.openwop.cron@1.0.0",
  "advice": "operator-facing copy (OPTIONAL)"
}

unmet[] is the subset of the pack's runtime.requires[] the host will not grant; manifest is the offending name@version; advice is OPTIONAL operator-facing remediation copy.

Composition with RFC 0069 (normative). runtime.requires: ["subprocess"] is the _install-time gate_ (will the host grant subprocess at all?); RFC 0069's exec-class contract is the _runtime contract_ for how subprocess is granted (exec-class enumeration, sandbox mechanics). They are independent so a host can adopt the gate without first implementing RFC 0069:

  • A host that grants runtime.requires: ["subprocess"] AND advertises RFC 0069's exec-class contract MUST apply that contract to every subprocess invocation by the pack.
  • A host MAY grant runtime.requires: ["subprocess"] without RFC 0069 — in which case its sandbox-internal subprocess mechanics are not protocol-specified, and a pack requiring exec-class-grade isolation SHOULD additionally express that dependency via peerDependencies (RFC 0072 §C), which fails install closed when unmet.
  • The two gates compose to the stricter of the two: peerDependencies is install-closed (unmet ⇒ refuse) and runtime.requires is install-closed; a pack declaring both runtime.requires: ["subprocess"] and peerDependencies: { "host.execClass": "supported" } opts into both and is refused on a host that grants subprocess but does not advertise the exec-class contract. That belt-and-suspenders posture is intentional and pack-author-selected, not a conflict.

§B — host-provided ctx.http.safeFetch (additive, OPTIONAL host capability)

Amendment (rev 4, 2026-05-29 — reconciled at implementation). §B's capability lives at capabilities.httpClient.safeFetch, not a standalone host.http.safeFetch. openwop already has an httpClient surface carrying the http-client-ssrf-guard invariant + maxResponseBodyBytes cap (the host's HTTP-client node egress); safeFetch is the _pack-facing exposure_ of that same SSRF-guarded client, so it nests under httpClient and reuses that invariant + body cap rather than standing up a parallel SSRF surface (the corpus's reuse-don't-duplicate posture, cf. RFC 0064). The normative behavior below is unchanged; only the advertisement path is reconciled. The living contract is host-capabilities.md §host.http.

A host MAY advertise capabilities.httpClient.safeFetch: { supported: true } and expose ctx.http.safeFetch(url, init?) to pack runtime code:

ctx.http.safeFetch(
  url: string,
  init?: RequestInit,        // method/headers/body subset, host-clamped
) → Promise<Response>        // standard fetch Response, or throws ssrf_blocked / fetch_failed

Behavior (normative, when advertised).

  • The host MUST perform SSRF defense before connecting: resolve the host, and reject (throw ssrf_blocked) any request whose resolved address is loopback, RFC 1918 private, link-local, or a cloud metadata endpoint (169.254.169.254, metadata.google.internal, etc.). The host MUST re-check the resolved address against the connected address to defeat DNS-rebinding (pin the resolved IP for the connection).
  • When capabilities.toolHooks.prePostEvents: true AND capabilities.httpClient.safeFetch.supported: true are both advertised, the host MUST emit agent.toolCalled / agent.toolReturned for every safeFetch invocation with transport: 'http'. Centralizing egress in the host must _increase_ auditability, not become a quiet bypass: a host that wishes to sample audit volume MUST do so at the storage/projection tier; the wire-level emission stays unconditional. (Same posture as RFC 0064's existing audit MUST for the agent.toolCalled/agent.toolReturned pair.) A host advertising safeFetch but not toolHooks.prePostEvents SHOULD still emit the pair.
  • A pack that uses ctx.http.safeFetch does not declare net.dns in runtime.requires for the fetch path — the host owns resolution. A pack that wants to run on hosts lacking the capability MAY feature-detect (ctx.http?.safeFetch) and fall back to its own net.outbound + net.dns path (declaring both).

This composes with — does not replace — RFC 0069's exec-class host-extension safety contract: safeFetch is the network-egress analogue of that RFC's subprocess sandboxing.

Example. core.openwop.http@2.0.0 (hypothetical) calls ctx.http.safeFetch(url) when present, dropping its in-pack assertPublicUrl + node:dns/promises; the host's audited blocklist applies uniformly across every pack that fetches.

Request-init clamping. The init? argument is a host-clamped subset of RequestInit, not a passthrough. The protocol does not number all clamps, but one is normative because it defends the same boundary as the SSRF check: the host MUST refuse connection-upgrade attempts (Connection: upgrade / Connection: keep-alive requesting a 101 protocol switch) — these defeat the resolve→pin→connect guard by escaping HTTP into a raw bidirectional socket. The rest are non-normative host-policy defaults, surfaced here so first implementations converge:

  • Body size — host-configurable cap (suggest 10 MB default).
  • Timeout — host-configurable (suggest 30 s default).
  • Headers — MAY be reshaped. A host SHOULD refuse to forward an Authorization header the pack did not construct from a credential the host itself issued (RFC 0046 host.credentials) rather than from a static manifest string, to prevent a pack exfiltrating a host-issued credential to an arbitrary URL — but enforcement is host policy. _(Promoting this SHOULD → MUST needs credential provenance expressible at the safeFetch boundary — see future RFC: credential provenance; tracked as Q5 in Unresolved questions.)_

Egress-proxy composition (non-normative). safeFetch composes with, and does not preclude, a deployment-level egress allowlist (e.g. Cloud Run behind a serverless VPC connector + Cloud NAT with its own allowlist). The host's resolve→pin→connect SSRF defense applies _before_ the egress proxy sees the request; the proxy's allowlist applies after — defense-in-depth, not redundancy.

Compatibility

Additive. Both surfaces are opt-in:

  • runtime.requires[] is a new OPTIONAL array; packs that omit it validate and load exactly as today. Hosts that don't gate platform access ignore it. The only new failure (pack_runtime_requirement_unmet) fires on packs that _opt in_ to a requirement the host won't grant — which today fails _anyway_ (at trial-load), only later and less legibly. No existing conformance pass is invalidated.
  • ctx.http.safeFetch is a new OPTIONAL sub-capability under the existing httpClient block (httpClient.safeFetch); hosts that omit it expose no ctx.http, and packs feature-detect. No existing pack depends on it.

No wire-event change, no new SECURITY invariant — the SSRF guarantee reuses the existing http-client-ssrf-guard invariant (and safeFetch reuses httpClient's maxResponseBodyBytes cap), no breaking schema change. Lands in v1.x.

Conformance

  • New, gated on a sandbox seam: a pack manifest declaring runtime.requires: ["subprocess"] against a host seam that denies subprocess MUST yield pack_runtime_requirement_unmet; a manifest with requires: ["net.dns"] against a host that grants it installs.
  • New, gated on httpClient.safeFetch.supported: ctx.http.safeFetch against a loopback / RFC-1918 / metadata URL MUST be blocked and MUST NOT connect; a DNS-rebinding case (public name re-resolving to 169.254.169.254) MUST be blocked; a request carrying Connection: upgrade MUST be refused. Driven by safefetch-behavior.test.ts via the POST /v1/host/sample/http/safe-fetch seam; soft-skips until a host advertises safeFetch + wires the seam.
  • New, gated on httpClient.safeFetch.supported + toolHooks.prePostEvents: a fetched safeFetch call emits the agent.toolCalled / agent.toolReturned pair with transport: 'http'. Two verification surfaces: safefetch-behavior.test.ts reads the pair the inline seam (POST /v1/host/sample/http/safe-fetch) echoes; safefetch-live-audit.test.ts asserts the pair was persisted to the durable run event log by the _production_ ctx.http.safeFetch path (via the POST /v1/host/sample/http/safe-fetch-run open seam + the test event-log seam). The live-run scenario closes the seam-vs-production gap — a host that ships a production createSafeFetch() with no audit hooks passes the inline seam but FAILS safefetch-live-audit.test.ts under OPENWOP_REQUIRE_BEHAVIOR=true. This live-run scenario is the §B → Accepted bar (the durable-emission MUST proven on a real host), alongside the core.openwop.http@2.0.0 consumer + non-steward adoption.
  • The advertisement contract (httpClientssrfGuard: true + positive maxResponseBodyBytes) stays covered by the existing http-client-ssrf.test.ts.
  • Validation: a manifest with a runtime.requires entry outside the vocabulary MUST be rejected invalid_manifest.

Both scenario groups soft-skip until the respective capability/seam is advertised, per the established gating convention.

Alternatives considered

1. Raw Node-builtin names in requires (MyndHyve's literal suggestion 1a, "listing required Node builtins"). Rejected as the normative vocabulary: node:dns/promises is meaningless for the Python / Go / wasm / remote runtimes the manifest already supports, and would leak a runtime's implementation surface into the portable wire contract. The abstract vocabulary (net.dns, …) captures the same gating intent portably. A host MAY map the abstract primitive to its runtime's concrete builtins internally. 2. Reuse NodeModule.requires / runtimeCapabilities for platform primitives. Rejected — that mechanism is _per-node, host-advertised opaque facilities_ checked at dispatch (chat.sendPrompt), a different axis and a different enforcement point. Overloading it would conflate "this node needs a host facility at run" with "this pack's code needs a sandbox primitive at load," and break the clean dispatch-time capability_not_provided semantics. 3. safeFetch only, no runtime.requires. Rejected — safeFetch solves only outbound HTTP; it does nothing for packs needing subprocess, fs, or crypto. The declaration is the general gate; safeFetch is one centralized facility under it. 4. Do nothing (status quo: trial-load). Rejected — trial-load defers a load-time contract failure to first production invocation, is non-portable across sandbox policies, and forces each operator to reverse-engineer a pack's platform needs (exactly MyndHyve's experience). The cost of doing nothing is paid by every future sandbox host integrating every future pack.

Unresolved questions

_Q1–Q4 resolved by the MyndHyve second-host comment-window review (2026-05-28); positions folded into the Proposal above._

1. Vocabulary scope — RESOLVED. Ship the v1 set as-is (now eight: net.dns, net.outbound, crypto, subprocess, fs.read, fs.write, env.read, clock). Reject coarser buckets (net/fs) — they lose gating power that is already paying off (a host can grant net.dns for safe-fetch preflight while denying net.outbound because all egress goes through safeFetch; collapsing to net would force granting the combined bucket). Defer finer buckets (net.outbound.http vs net.outbound.raw) — a real distinction (fetch allowed, raw sockets denied) but additively extensible later without breaking anyone (enum-addition, wasm-* precedent), so it stays out of v1. env.read added on review. 2. safeFetch audit emission — RESOLVED: MUST when both advertised. See §B normative bullet. Sampling belongs at the storage/projection tier, not the wire-emission tier (RFC 0064 posture). 3. Relationship to RFC 0069 — RESOLVED: independent declarations + composition MUST. See §A "Composition with RFC 0069." The gate and the runtime contract stay decoupled so a host can adopt the gate without first implementing RFC 0069. 4. Non-sandbox host strictness — RESOLVED: SHOULD record. A non-gating host SHOULD project runtime.requires[] onto the inventory entry for operator visibility (§A). One denormalized field copy; cost-to-value favors SHOULD.

Genuinely still open:

1. Authorization-header forwarding enforcement (PARKED — own RFC). §B leaves "refuse forwarding a pack-constructed Authorization not derived from a host-issued credential" as host policy (SHOULD). Promoting it to MUST needs credential provenance at the safeFetch boundary, distinguishing a host-issued credential from a static manifest string at call time. Two candidate mechanisms, each RFC-worthy on its own (folding either into 0076 would balloon scope): (a) thread provenance through RequestInit (intrusive — changes the call signature), or (b) a host-side credential-issuance ledger keyed by call-id. Deferred to a dedicated "credential provenance" RFC; the §B breadcrumb points here.

Implementation notes (non-normative)

  • Reference host (examples/hosts/*): add an install-time check that intersects runtime.requires[] against a configured grant-set, emitting pack_runtime_requirement_unmet; add a ctx.http.safeFetch behind a config flag implementing the resolve→pin→connect SSRF guard. Effort: small for the gate, medium for the rebinding-safe fetch.
  • core.openwop.http is the natural first adopter of both: declare runtime.requires: ["net.dns", "net.outbound"] now (additive, no behavior change), and ship a safeFetch-preferring 2.0.0 once a reference host advertises the capability.
  • Approval-snapshot under RFC 0074 (raised in second-host review). A workspace approval today records {packName, approvedAt, approvedBy}. Once runtime.requires lands, an approval captures a _single grant-evaluation_; a later host update that tightens its sandbox can silently invalidate previously-approved packs (the dispatch-time gate now fails). Hosts SHOULD snapshot the gate-evaluated runtime.requires onto the approval document (approved against {net.dns, net.outbound} on 2026-05-28) so admins see the grant set an approval was made against and can react explicitly when it shifts. Non-normative for this RFC; a candidate RFC 0074 amendment.
  • Sequencing (§A before §B). §A (declaration + install gate) is independently shippable and delivers the immediate win the debrief surfaced; it merges to Active first, graduating to Accepted on one second-host adoption. §B (safeFetch) needs a reference implementation + ≥1 consumer pack gating behind feature-detection, so it follows as a separate Active → Accepted track once a reference host implements it and core.openwop.http@2.0.0 ships the feature-detection path. This mirrors the established cohort-graduation pattern (RFC 0058's wall-clock and loop-iteration arms graduated independently).

Acceptance criteria

Two independent tracks (see Implementation notes §"Sequencing").

§A — runtime.requires[] (declaration + install gate):

  • [x] Spec text merged (this file + runtime.requires in node-packs.md §"Runtime platform requirements" + the install gate + pack_runtime_requirement_unmet payload in registry-operations.md §"Runtime-requirement install gate"). _(2026-05-29)_
  • [x] schemas/node-pack-manifest.schema.json adds runtime.requires (8-value oneOf/const/description). _(2026-05-29)_
  • [x] ≥1 conformance scenario (install-grant, install-refuse, vocabulary-validation), seam-gated. _(2026-05-29 — runtime-requires-shape.test.ts always-on + runtime-requires-install-gate.test.ts seam-gated; suite 1.10.0, unreleased additions)_
  • [x] CHANGELOG entry under the target v1.x. _(2026-05-29)_
  • [x] Active → Accepted 2026-05-29 — verified live on MyndHyve (non-steward host). runtime-requires-install-gate.test.ts (suite 1.10.0, unreleased additions) passes all three behavioral scenarios under OPENWOP_REQUIRE_BEHAVIOR=true against workflow-runtime-00230-zjn (https://workflow-runtime-gjw5bcse7a-uc.a.run.app, OPENWOP_RUNTIME_REQUIRES_GATE_ENABLED=true + OPENWOP_CONFORMANCE_FIXTURES=1): install-grant → 200 {outcome:"installed"}, install-refuse → 400 {error:"pack_runtime_requirement_unmet", manifest, unmet, advice}, non-sandbox projection → 200 {…, requiresProjected}. Steward-corroborated by independent seam curls; discovery confirms §B unclaimed. Pack-side adoption: 6 vendor.myndhyve.* packs declare runtime.requires[] (4d36c15d); host-side gate: MyndHyve 4d2ba6553 (resolver) + 4bb1ab4f6 (seam).

§B — ctx.http.safeFetch (separate Active → Accepted track):

  • [x] Spec text merged (§host.http in host-capabilities.md documenting ctx.http.safeFetch + the SSRF/clamp/audit contract); schemas/capabilities.schema.json adds httpClient.safeFetch (and schematizes the previously-unschematized httpClient block — ssrfGuard/maxResponseBodyBytes/requestTimeoutMs/methods). _(2026-05-29, reconciled per the §B amendment — httpClient.safeFetch, not host.http.safeFetch.)_
  • [x] ≥1 conformance scenario (SSRF block incl. rebinding + Connection: upgrade refusal; audit-emission-when-both-advertised), capability+seam-gated — safefetch-behavior.test.ts; advertisement contract via the existing http-client-ssrf.test.ts. _(2026-05-29)_
  • [x] Live-run audit scenario authored (the §B → Accepted bar) _(2026-05-29)_ — safefetch-live-audit.test.ts asserts the audit-when-both MUST against the durable run event log (production ctx.http.safeFetch path), not the inline seam echo, via the new POST /v1/host/sample/http/safe-fetch-run open seam + the test event-log seam; behaviorGate('openwop-safefetch-live-audit', …) so a host advertising both flags but not emitting to the durable log FAILS under OPENWOP_REQUIRE_BEHAVIOR=true. Closes the seam-vs-production gap surfaced in the §B second-host review (a production createSafeFetch() with no audit hooks passes the inline seam). Steward-side spec/test; a non-steward host validates its wiring against it.
  • [x] Reference host implements the resolve→pin→connect guard _(2026-05-29)_ — the in-memory reference host advertises capabilities.httpClient.safeFetch and implements the POST /v1/host/sample/http/safe-fetch seam (SSRF blocklist on loopback / RFC 1918 / link-local / cloud-metadata, DNS-rebinding defeat via the pinned-IP model, Connection: upgrade refusal, and the tool-hooks audit pair since it also advertises toolHooks.prePostEvents). safefetch-behavior.test.ts (5/5) + http-client-ssrf.test.ts pass under OPENWOP_REQUIRE_BEHAVIOR=true against it.
  • [x] core.openwop.http@2.0.0 ships the feature-detection consumer path _(2026-05-29, source)_ — every outbound call feature-detects ctx.http?.safeFetch (host-mediated, preferred) with a fallback to the pack's own assertPublicUrl (node:dns) + globalThis.fetch; declares runtime.requires: ["net.dns","net.outbound"] (§A). Both paths verified by a node smoke.
  • [x] Registry republish of the signed core.openwop.http@2.0.0 tarball _(2026-05-29)_ — built + signed (openwop-team-1) via build-pack-tarball.mjs; the in-tree registry mirror (registry/v1/packs/core.openwop.http/-/2.0.0.{tgz,sig,json,sbom.json} + the unpacked registry/core.openwop.http/2.0.0/ schemas) regenerated via registry/scripts/build-index.mjs + generate-sbom.mjs (latest → 2.0.0). All registry gates green: verify-signatures, check-pack-tarball-signatures, check-registry-signer-consistency, conformance-check, integrity match.
  • [ ] Remaining §B Active → Accepted gate: a non-steward host adopting ctx.http.safeFetch — MyndHyve committed to the host adapter + flipping net.outbound into its grant-set (which unblocks its ads-publish-* packs from the §A install refusal).

References

  • MyndHyve RFC 0072 §B second-host debrief, 2026-05-28 (finding 1a/1b) — the implementer pain point that prompted this RFC.
  • MyndHyve RFC 0076 second-host review, 2026-05-28 — Q1–Q4 positions + observations A–D (approval-snapshot, request-init clamping, error payload, egress-proxy) + the §A/§B sequencing recommendation, all folded into rev 2.
  • MyndHyve RFC 0076 §A schema-diff review, 2026-05-29 — five mechanical schema fixes (stale enum/elision, JSON-comment removal via oneOf, empty-array equivalence, coarser-parent forward-compat, closed-enum safety) + the peerDependencies-compose note, folded into rev 3. wasm-component (RFC 0008.1) drop-risk caught here.
  • RFC 0003 — agent/pack manifest (runtime object this extends).
  • RFC 0072 — agent inventory + dispatch (peerDependencies / peerDependenciesMeta, the adjacent host-capability gate).
  • RFC 0069 — exec-class tool host-extension safety contract (subprocess sandboxing; safeFetch is its network analogue).
  • RFC 0064 — host.toolHooks (the audit surface safeFetch egress reuses).
  • spec/v1/capabilities.md §"Runtime capabilities" — the distinct per-node host-facility mechanism this deliberately does not overload.
  • OWASP SSRF prevention cheat sheet; GCP/AWS metadata-endpoint hardening guidance (prior art for the blocklist).