Skip to content

feat: ephemeral.* namespace + discoverable annotation pilot [#25]#26

Merged
tercel merged 2 commits intomainfrom
feat/ephemeral-modules-pilot
May 6, 2026
Merged

feat: ephemeral.* namespace + discoverable annotation pilot [#25]#26
tercel merged 2 commits intomainfrom
feat/ephemeral-modules-pilot

Conversation

@tercel
Copy link
Copy Markdown
Contributor

@tercel tercel commented May 5, 2026

Closes #25.

Pilot disclaimer. Implements pilot ahead of upstream RFC acceptance. The apcore RFC docs/spec/rfc-ephemeral-modules.md is currently in Draft / RFC state. Maintainer should decide whether to merge this before or after upstream accept. Downstream SDKs (apcore-typescript, apcore-rust) will follow once Python pilot findings are reported back to the RFC.

What this does

Pilots the apcore RFC for a sanctioned ephemeral.* namespace + discoverable: false annotation that gives a name and minimum guardrails to programmatically-registered modules synthesized at runtime by LLM-agent pipelines (e.g. ToolMaker, ACL 2025, arXiv 2502.11705).

1. Reserve ephemeral.* namespace

  • Filesystem discovery now raises InvalidInputError (code INVALID_MODULE_ID) for any path that would derive an ephemeral.* ID — single-root and multi-root with namespace="ephemeral" are both rejected.
  • The namespace is reachable only via Registry.register(module_id, module, ...). New top-level export: EPHEMERAL_NAMESPACE_PREFIX from apcore.registry.registry.

2. discoverable: bool = True annotation

  • New field on ModuleAnnotations. Default True preserves backward compat.
  • When False, the module is filtered out of Registry.list() / Registry.iter() / Registry.module_ids (and downstream manifest export which iterates module_ids).
  • Hidden modules remain callable via Registry.get() / Executor.execute() when the caller already knows the ID, per the RFC.
  • New include_hidden=True keyword on list() / iter() returns the full set for introspection tools.

3. Audit events

  • New Registry.set_event_emitter(emitter) opt-in. When wired, ephemeral.* register/unregister emit canonical apcore.registry.module_registered / apcore.registry.module_unregistered events.
  • Payload mirrors the D-35 contextual-audit shape used by system.control.* modules: caller_id (default "@external" if no identity) plus a redacted identity snapshot when context.identity is set. Sensitive identity attrs (bearer_token, secret, etc.) are replaced with "<redacted>" via the existing _audit_payload_extras helper.
  • Registry.register() / Registry.unregister() accept an optional context= kwarg used solely to enrich the audit payload.
  • Without an emitter the audit info is logged at INFO so it never silently disappears.

4. Soft warning for missing requires_approval

  • A logging.warning(...) fires when an ephemeral.* module is registered without requires_approval=True, citing the RFC and recommending the caller set it.
  • This is a soft signal — registration still succeeds.

5. Lifecycle: caller-managed only

  • Registry.unregister() only. No TTL or GC sweeper — deferred to v2 per the RFC's pilot scope.

Out of scope (deliberate)

  • TTL / GC sweeper — deferred to v2 per RFC.
  • Sandboxing of agent-synthesized code bodies — host concern per RFC.

Tests

25 new tests in tests/registry/test_ephemeral_modules.py, organized into four classes:

  • TestNamespaceReservation (5) — filesystem discovery rejection, programmatic register/unregister happy paths.
  • TestDiscoverableAnnotation (9) — default value, every enumeration surface, YAML override, hidden-but-callable.
  • TestRequiresApprovalWarning (5) — warning emitted, suppressed when annotation present, no false-positive for non-ephemeral, dict-style annotations.
  • TestEphemeralAuditEvents (6) — register/unregister emits, full D-35 payload with identity redaction, no emit for non-ephemeral modules, INFO-log fallback when no emitter wired.

Full suite: 2897 passed, 2 skipped (was 2872 — +25 new). ruff check clean.

Files changed

  • src/apcore/module.pydiscoverable field on ModuleAnnotations.
  • src/apcore/registry/registry.py — namespace reservation, audit emit, soft warning, discoverable filter, set_event_emitter(), EPHEMERAL_NAMESPACE_PREFIX export.
  • tests/registry/test_ephemeral_modules.py — new test suite.
  • tests/test_conformance.py — pilot-tolerant normalisation: strips discoverable from the serialized form when the upstream cross-language conformance fixture predates the field, so all other keys remain strictly compared.
  • CHANGELOG.md — entry under ## [Unreleased]### Added with full pilot disclaimer.

RFC-feedback notes for the maintainer

A few small design choices were forced by the existing codebase that are worth flagging back to the RFC:

  1. Audit-event delivery path. The existing _bridge_registry_events helper in apcore.sys_modules.registration already emits empty-payload apcore.registry.* events for all registrations via the callback bridge. The pilot adds a separate registry-side direct emit for ephemeral.* only, with the contextual payload. When both register_sys_modules() and set_event_emitter() are wired, an ephemeral.* registration produces two events with the same event_type (one with empty data, one with the audit payload). This is fine for the pilot but the RFC should specify whether the bridge eventually subsumes the registry direct-emit, or whether the bridge gains context-awareness.
  2. discoverable interaction with register_internal(). register_internal() calls merge_module_metadata directly, so it honours discoverable=False set in code. The _is_discoverable helper falls back to the module instance's annotations attribute when _module_meta["annotations"] is absent, so both registration paths behave consistently.
  3. Cross-language consistency. The conformance fixture annotations_extra_round_trip does not yet include discoverable. The Python test was made pilot-tolerant; the canonical fixture under apcore/conformance/fixtures/ will need a follow-up update once the RFC is accepted.

🤖 Generated with Claude Code

Pilot implementation of the apcore RFC docs/spec/rfc-ephemeral-modules.md
(Draft / RFC) for programmatically-registered modules synthesized at
runtime by LLM-agent pipelines (e.g. ToolMaker, ACL 2025).

- Reserve `ephemeral.*` namespace: filesystem discovery raises
  InvalidInputError for any path that would derive an `ephemeral.*` ID;
  the namespace is only reachable via Registry.register().
- Add `discoverable: bool = True` field to ModuleAnnotations. When False,
  the module is filtered out of Registry.list(), Registry.iter(), and
  Registry.module_ids while remaining callable via Registry.get().
  Callers that need the full set pass include_hidden=True.
- Audit events: new Registry.set_event_emitter() opt-in. When wired,
  ephemeral.* register/unregister emit canonical
  apcore.registry.module_registered / module_unregistered events with a
  D-35 contextual-audit payload (caller_id default "@external" plus a
  redacted identity snapshot when context.identity is set). Without an
  emitter the event falls back to an INFO log so it is never silently
  dropped.
- Soft warning: logging.warning(...) fires when an ephemeral.* module
  registers without requires_approval=True, citing the RFC. Registration
  itself succeeds.
- Lifecycle is caller-managed via Registry.unregister(); TTL/GC sweeper
  and host-side code sandboxing are deferred per the RFC's v1 scope.

New top-level export: EPHEMERAL_NAMESPACE_PREFIX from
apcore.registry.registry.

Tests: 25 new cases in tests/registry/test_ephemeral_modules.py covering
namespace reservation enforcement, discoverable filter on every
enumeration surface, audit event emission with full D-35 redaction
behaviour, and the soft requires_approval warning. Full suite: 2897
passed, 2 skipped.

The cross-language conformance fixture for ModuleAnnotations does not
yet include `discoverable`; the test was updated to tolerate the pilot
field when absent from `expected_serialized` so cross-language equality
remains meaningful for every other key. Conformance fixtures will be
updated upstream once the RFC is accepted.

Signed-off-by: tercel <tercel.yi@gmail.com>
@tercel
Copy link
Copy Markdown
Contributor Author

tercel commented May 5, 2026

Upstream RFC clarified (apcore@81df336)

Both ambiguities you flagged in the PR description are now resolved in upstream rfc-ephemeral-modules.md:

1. Dual-emit → single-emit rule

For ephemeral.* registrations, exactly one event MUST be emitted with 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 no identity
  identity: <object | null>    # redacted snapshot per D-35
  namespace_class: ephemeral

For non-ephemeral.* registrations, the existing _bridge_registry_events empty-payload behavior is preserved (backward compat).

Implementation choice is per-SDK: short-circuit the bridge for ephemeral.* IDs OR extend the bridge to be context-aware globally. Either is acceptable.

For this PR: the current dual-emit pattern is correct as a pilot but should be tightened before merge. Recommend: add a check in _bridge_registry_events that skips the empty-payload emit when the module_id starts with ephemeral. (since the registry-side direct emit is already firing for those). One-line guard, ~3 LOC.

2. register_internal() interaction → MUST reject

Upstream rule: register_internal() MUST reject ephemeral.* IDs with a clear error pointing the caller to use Registry.register(). Rationale: namespace → registration-mechanism is a 1:1 mapping; mixing blurs the audit-trail distinction between framework-emitted (system.*) and caller-emitted (ephemeral.*).

For this PR: please add the rejection guard in register_internal() with a clear ValueError message + a unit test. Suggested error: "ephemeral.* module IDs must be registered via Registry.register() (not register_internal()); see apcore docs/spec/rfc-ephemeral-modules.md".

3. Transitional fixture handling

Your pilot-tolerant approach (strip discoverable from comparison when upstream fixture lacks it) is now the canonical bridge pattern documented in the RFC. apcore-typescript and apcore-rust will adopt the same pattern when they ship discoverable support. No change needed here.

Full RFC update: apcore commit 81df336, iter-11 addendum in docs/spec/2026-05-decision-log.md.

🤖 (auto-comment from upstream RFC iter-11 reconciliation)

…r_internal rejection)

Tightens the ephemeral.* pilot against two new normative rules added to the
upstream apcore RFC `docs/spec/rfc-ephemeral-modules.md` during the iter-11
reconciliation round (apcore commit 81df336):

1. Audit-event single-emit rule (RFC "Audit-event single-emit rule")
   `_bridge_registry_events` now short-circuits for ephemeral.* module IDs so
   that exactly ONE apcore.registry.module_registered /
   apcore.registry.module_unregistered event is emitted per registration --
   the rich registry-side direct emit carrying the full D-35 contextual
   payload -- instead of being followed by a second empty-payload copy from
   the bridge. Non-ephemeral registrations are unaffected (legacy bridge
   behaviour preserved for backwards compatibility).

2. register_internal() rejection (RFC "register_internal() interaction")
   Registry.register_internal() now raises ValueError when called with an
   ephemeral.* module ID, directing the caller to Registry.register().
   Rationale: namespace -> registration-mechanism is a 1:1 mapping; mixing
   the two paths blurs the audit-trail distinction between framework-emitted
   (system.*) and caller-emitted (ephemeral.*) modules.

Tests
- New `test_bridge_does_not_dual_emit_for_ephemeral` in
  TestEphemeralAuditEvents: wires both `set_event_emitter` and
  `_bridge_registry_events` and asserts exactly one canonical event (rich
  payload, not empty), and that the legacy `module_registered` alias is
  also suppressed for ephemeral.*.
- New `TestRegisterInternalRejectsEphemeral` class with two cases: rejection
  raises ValueError + state is not mutated, and the error message points the
  caller to `Registry.register()`.

Full suite: 2900 passed, 2 skipped (was 2897 + 3 new). ruff check clean.

Refs: aiperceivable/apcore@81df336
Signed-off-by: tercel <tercel.yi@gmail.com>
@tercel tercel merged commit 8b7b69c into main May 6, 2026
2 checks passed
@tercel tercel deleted the feat/ephemeral-modules-pilot branch May 6, 2026 00:17
tercel added a commit that referenced this pull request May 6, 2026
Aligns apcore-python with PROTOCOL_SPEC.md v0.21.0:
- §5.6 Module: newly added optional preview() method (sync + async,
  detected via hasattr — mirrors existing preflight() pattern).
- §12.8 type contracts: new Change and PreviewResult pydantic models;
  PreflightResult.predicted_changes field; module_preview enum entry on
  PreflightCheckResult.check.
- Change uses the Python idiomatic encoding from RFC rfc-preview-method.md
  ("Change.x-* extension fields" cross-SDK table): pydantic
  ConfigDict(extra='allow') paired with a model-validator that rejects
  extra keys not matching ^x-.
- Executor.validate() invokes preview() after standard checks, awaits
  coroutine results, folds PreviewResult.changes into
  PreflightResult.predicted_changes, and emits a module_preview check.
  Exception semantics mirror preflight(): raised exceptions become
  warnings on the module_preview check; validation does not fail.

Stage 3 (ephemeral.* + discoverable + audit single-emit + register_internal
rejection) was previously shipped in #26 + iter-11 alignment commits — its
CHANGELOG entries are simply promoted out of [Unreleased] into [0.21.0].

Cross-refs: apcore commit c191b85 (RFC promotion to PROTOCOL_SPEC v0.21.0),
apcore-typescript PR #29 (reference impl for the preview pattern).

Tests: 2909 passing (+9 vs baseline — 7 preview tests covering
not-present/None/result/async/exception + 2 importable-API tests).

Version bumped to 0.21.0 in pyproject.toml.

Signed-off-by: tercel <tercel.yi@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC pilot: ephemeral.* namespace + discoverable annotation (apcore#54)

1 participant