Accepted (2026-05-06). Promoted to protocol-spec.md normative sections in v0.21.0:
§2.5 Reserved Words—ephemeraladded to framework reserved list; namespace semantics table now listsephemeral.*with its registration / discoverability / annotation contract§4.4 ModuleAnnotations— newdiscoverable: booleanannotation field (defaulttrue); RFC's "single-emit rule" and "register_internal() interaction" rules are normative
The pilot in apcore-python (PR #26) shipped the full v1 surface: namespace reservation, discoverable annotation, audit-event single-emit, register_internal() rejection, soft-warning on missing requires_approval. apcore-typescript and apcore-rust follow-up implementations are tracked in their respective repos.
This RFC document is retained as design rationale + cross-SDK pilot reference. Implementations should consult protocol-spec.md for the normative contract.
A growing class of LLM-agent workflows synthesize callable units at runtime:
- Code-generation agents that compile a Python function from a task description and a repository URL (ToolMaker, ACL 2025, arXiv 2502.11705 — 80% task implementation correctness vs. 20% baseline).
- On-the-fly composition of existing modules into a new callable surface (LATM and follow-on work in the LLM-ToolMaker line).
- Per-session "scratch tools" assembled by an orchestrator and discarded after the session ends.
Today, the canonical apcore registration path is filesystem-based (protocol-spec.md §2.1 Algorithm A01: directory path → canonical Module ID). Authors of agents that synthesize tools at runtime have to either:
- Reuse
Registry.register(module_id, instance)ad-hoc, but with no spec'd convention for naming, lifecycle, or audit; or - Manage a parallel registry outside apcore, losing ACL, audit, and observability.
This RFC proposes a sanctioned ephemeral.* namespace that gives a name + minimum guardrails to programmatically-registered modules without disturbing the filesystem-rooted core.
A common misreading is that "Module ID = directory path" is a runtime invariant. It is not.
- §2.1 Algorithm A01 is the scanner-stage path-to-ID conversion. It governs how the default discoverer derives canonical IDs from filesystem layout. It does not constrain
Registry.register(). - §2.1 trailing paragraph (around line 256) specifies the only runtime requirement for
Registry.register(module_id, ...):module_idmatches the canonical regex^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$. - §6.6.1
register_internal()(around line 3598) is the existing precedent:system.*modules are programmatically registered with caller-supplied IDs that have no filesystem origin. The spec already accepts non-filesystem programmatic registration; this RFC just gives a class of such modules a name and a discipline.
A four-round audit (PROTOCOL_SPEC text + features docs + Module-interface contract + 3 SDK source reads) confirmed that all three SDKs already accept programmatic registration with caller-supplied IDs:
apcore-pythonRegistry.register(module_id: str, module: Any, ...)—registry.py:857-965.apcore-typescriptregister(moduleId: string, module: unknown, ...)—registry/registry.ts:562-587.apcore-rustregister(name: &str, module: Box<dyn Module>, descriptor: ModuleDescriptor)—registry.rs:469-476. TheModuletrait has noid()method; ID is registry metadata, not module-instance state.
So the implementation primitives all exist. What's missing is convention + audit + lifecycle.
- Sandboxing. Code execution isolation (Wasm, container, language VM) is the host's responsibility. Spec only governs registration, ACL, audit, and lifecycle.
- Codegen. This RFC does not specify how an agent generates the module body; that's pipeline territory (see ToolMaker for a reference design).
- Replacing filesystem discovery.
ephemeral.*is additive. Filesystem-rooted modules retain their conventions unchanged.
Add a new entry to protocol-spec.md §2.5 reserved namespaces, parallel to system.* and core.*:
| Namespace | Purpose | Registration | Discoverable | Mandatory annotations |
|---|---|---|---|---|
system.* (existing) |
Built-in framework modules | register_internal() only |
Yes | (existing) |
core.* (existing) |
Reserved for future spec promotion | (reserved — none yet) | — | — |
ephemeral.* (new) |
Programmatically-generated runtime modules | Standard register() |
No (see annotation below) | requires_approval: true |
Modules in the ephemeral.* namespace:
- MAY be registered via standard
register(module_id, module, ...)(in contrast tosystem.*, which usesregister_internal()). - SHOULD declare
requires_approval: trueto prevent agent-synthesized tools from running unattended. - SHOULD declare
discoverable: false(a new annotation; see below). - MUST be subject to ACL
default_effect: deny. ACLtargetpatterns supportingephemeral.*wildcards already work (per existing first-match-wins evaluation). - MUST emit audit events through the framework
EventEmitter, mirroringsystem.control.*write modules' contextual-audit shape (D-35). See "Single-emit rule" below. - SHOULD declare a TTL (open question: convention key — see below).
A new boolean annotation on Module:
annotations:
discoverable: false # default: true; ephemeral.* modules SHOULD set falseSemantics:
discoverable: true(default) — module appears inRegistry.list(),Registry.find(), manifest export, MCPtools/list, etc.discoverable: false— module is callable throughRegistry.invoke()/Executor.execute()but is hidden from enumeration surfaces. Caller must already know the module ID.
This is independently useful: filesystem-rooted modules can also opt out of enumeration (e.g., for internal-only utilities), and existing internal: true patterns in some SDK ports converge on the same idea.
A four-round audit during the apcore-python pilot surfaced a dual-emit risk: all three SDKs already have a registry-event bridge (apcore-python _bridge_registry_events, apcore-typescript sys-modules/registration.ts, apcore-rust RegistryEvents callback) that fires apcore.registry.module_registered / apcore.registry.module_unregistered with an empty payload for every registration. Naïvely adding a second contextual emit for ephemeral.* registrations produces two events with the same event_type for the same registration — bad audit hygiene.
Rule: For any single registration / unregistration of an ephemeral.* module, exactly one event MUST be emitted, carrying the full contextual payload:
event_type: apcore.registry.module_registered # or .module_unregistered
payload:
module_id: ephemeral.<name>
caller_id: <string> # defaults to "@external" when context.caller_id is None/null/""
identity: <object | null> # redacted snapshot per D-35; null when no identity is set
namespace_class: ephemeralFor non-ephemeral.* registrations, the existing bridge's empty-payload behavior is preserved (backward compatibility for current subscribers).
Implementation choices (per-SDK, not normative):
- Short-circuit the bridge for
ephemeral.*IDs and emit only the rich version, OR - Extend the bridge to be context-aware for all registrations (richer for everyone, opt-in by the SDK)
Subscribers SHOULD treat additional payload fields as forward-compatible (existing empty-payload subscribers continue working without modification).
apcore-python exposes register_internal() as a programmatic registration path historically reserved for system.* modules. The pilot surfaced an open question: should register_internal() accept ephemeral.* IDs?
Rule: register_internal() (or its equivalent in any SDK) MUST reject ephemeral.* IDs with a clear error pointing the caller to use Registry.register(). Rationale:
- Audit-trail provenance:
system.*events carry framework-emitted provenance;ephemeral.*events carry caller-emitted (Agent / user) provenance. Mixing the two backdoors blurs forensics. - ACL enforcement: standard
register()runs the full ACL + audit pipeline;register_internal()typically bypasses ACL because system modules are framework-owned.ephemeral.*modules are not framework-owned and MUST go through ACL. - Principle: namespace prefix → registration mechanism is a 1:1 mapping.
system.*only viaregister_internal().ephemeral.*only viaregister(). No overlap.
For SDKs that don't have a register_internal() distinction (apcore-typescript, apcore-rust), this rule is automatically satisfied.
Two designs are on the table:
- Caller-managed.
ephemeral.*modules live until the caller explicitly callsRegistry.unregister(module_id). Simple; matchesregister_internal()precedent. Risk: leakage if agent crashes mid-session. - TTL-driven. Modules declare a TTL (e.g.,
x-ephemeral-ttl-seconds: 3600); framework runs a sweeper that unregisters expired entries. MirrorsReaperfor async tasks. Risk: another background loop to manage.
Recommendation for pilot: start with caller-managed (cheaper); add TTL as a v2 follow-up if leakage is observed in practice.
Apcore does not specify how ephemeral.* module bodies are sandboxed. Hosts that accept agent-synthesized code (e.g., apcore-mcp bridges, apcore-cli plugins) MUST apply their own isolation:
- Process-level (subprocess with restricted environment).
- Wasm runtime (if SDK ecosystem supports).
- Language-level (Python
RestrictedPython, V8 isolates,wasmtimefor Rust-loaded user code).
The spec's responsibility is bounded at registration → ACL → audit → lifecycle. It is not bounded at code execution.
- Pilot in
apcore-pythonfirst — already hasregister_internal()precedent and the most permissive runtime. Pilot exposes:ephemeral.*namespace as reserved (no enforcement yet, just convention).discoverable: falseannotation honored byRegistry.list().- Audit-event emission on
register()andunregister()forephemeral.*IDs.
- Track via tracking issue — file against
apcore-pythonrepo. Stage outcome reports intodocs/spec/2026-05-decision-log.md. - Promote to spec when pilot confirms ergonomic — at that point, this RFC becomes normative §2.5 + §4.4 amendments.
- TypeScript and Rust SDKs follow — once Python pilot ships, replicate.
The discoverable annotation field is a normative addition to ModuleAnnotations once this RFC is accepted. During the rollout window where some SDKs have shipped discoverable and others have not, the canonical conformance fixture conformance/fixtures/annotations_extra_round_trip.json MUST NOT be updated to require the field — doing so would actively break the conformance tests of SDKs that have not yet shipped support. Instead:
- SDKs that ship
discoverableMAY make their conformance test runner pilot-tolerant: when comparing anexpected_serializedblock that lacksdiscoverable, strip the field from the actual serialized output before equality comparison. - Once all three SDKs have shipped
discoverable, a follow-up apcore PR updatesannotations_extra_round_trip.json: adddiscoverable: true(the default) to each test case'sinputandexpected_serialized, and add a new test casediscoverable_false_round_tripcovering the explicit-false path. After that PR lands, the pilot-tolerant code paths in each SDK can be removed.
apcore-python PR #26 (the pilot) implements the pilot-tolerant pattern. apcore-typescript and apcore-rust follow-up PRs SHOULD do the same until the synchronized fixture update.
conformance/fixtures/ephemeral_modules.json (not created in this RFC stage):
- Register
ephemeral.test_v1programmatically; assertRegistry.list()does not include it;Registry.invoke("ephemeral.test_v1", ...)succeeds. - ACL
target: "ephemeral.*"rule applies first-match-wins as expected. - Audit event
apcore.registry.module_registeredemitted withmodule_id: "ephemeral.test_v1"on register. - Audit event
apcore.registry.module_unregisteredemitted on unregister. - Re-registering same
ephemeral.*ID replaces (or rejects per spec — open question for pilot). requires_approval: falseon anephemeral.*module produces a SHOULD-warning (not error) at registration.
- Namespace name.
ephemeral.*vsvolatile.*vstemp.*vsdynamic.*vsagent.*. Recommendation:ephemeral.*— most precise about lifecycle without implying source ("dynamic" is too broad; "agent" conflates with caller identity). - Annotation name.
discoverable: falsevsinternal: truevshidden: true. Recommendation:discoverable: falsebecause it directly names the property being toggled. - TTL key name (if pursued).
x-ephemeral-ttl-secondsvsx-ttl-secondsvs annotationttl_seconds. Recommendation: keep asx-*convention initially; promote to first-class annotation only if usage justifies. - Re-registration semantics. Allow
register()to overwrite an existingephemeral.*ID, or requireunregister()first? Recommendation: require unregister; agents can clear-and-rebuild explicitly. - Cross-language collision risk. Should the spec reserve
ephemeral.*even before pilot completes, to prevent third-party use? Recommendation: yes — file a 1-line spec patch reserving the namespace immediately, separate from this full RFC's acceptance. - Interaction with
apcore-mcptools/list. Shoulddiscoverable: falsemodules be exposed through MCPtools/list? Recommendation: no by default; MCP servers can opt in via configuration if their host needs it.
- ToolMaker (ACL 2025, arXiv 2502.11705 / GitHub: KatherLab/ToolMaker) — autonomous tool creation from scientific code repositories with closed-loop self-correction; 80% task implementation correctness vs. 20% baseline. Real, verified.
- LATM (LLM-ToolMaker) — earlier work in the same line.
- Dynamic Tool Generation survey topic — emergentmind, ai-scholar coverage.
ToolMaker is a pipeline (codegen + sandbox + verify); apcore's role under this RFC is to provide the registration surface a ToolMaker-like pipeline lands its outputs into. The two are complementary.
- §2.1 — Module ID specification and
Registry.register()regex constraint. - §2.5 — reserved-words registry (where
ephemeral.*would be added). - §4.4 — ModuleAnnotations (where
discoverablewould be added). - §6.6.1 —
register_internal()(existing precedent for non-filesystem registration). - §4.6 D-57 —
x-supports-dry-run,x-required-context-keys,x-reasoning-demand(registered in companion patch). docs/spec/rfc-preview-method.md— sibling RFC.docs/features/system-modules.mdD-35 (contextual auditing for control plane) — audit-event shapeephemeral.*should mirror.