feat: ephemeral.* namespace + discoverable annotation pilot [#25]#26
feat: ephemeral.* namespace + discoverable annotation pilot [#25]#26
Conversation
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>
|
Upstream RFC clarified (apcore@81df336) Both ambiguities you flagged in the PR description are now resolved in upstream 1. Dual-emit → single-emit ruleFor 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: ephemeralFor non- Implementation choice is per-SDK: short-circuit the bridge for For this PR: the current dual-emit pattern is correct as a pilot but should be tightened before merge. Recommend: add a check in 2.
|
…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>
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>
Closes #25.
What this does
Pilots the apcore RFC for a sanctioned
ephemeral.*namespace +discoverable: falseannotation 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.*namespaceInvalidInputError(codeINVALID_MODULE_ID) for any path that would derive anephemeral.*ID — single-root and multi-root withnamespace="ephemeral"are both rejected.Registry.register(module_id, module, ...). New top-level export:EPHEMERAL_NAMESPACE_PREFIXfromapcore.registry.registry.2.
discoverable: bool = TrueannotationModuleAnnotations. DefaultTruepreserves backward compat.False, the module is filtered out ofRegistry.list()/Registry.iter()/Registry.module_ids(and downstream manifest export which iteratesmodule_ids).Registry.get()/Executor.execute()when the caller already knows the ID, per the RFC.include_hidden=Truekeyword onlist()/iter()returns the full set for introspection tools.3. Audit events
Registry.set_event_emitter(emitter)opt-in. When wired,ephemeral.*register/unregister emit canonicalapcore.registry.module_registered/apcore.registry.module_unregisteredevents.system.control.*modules:caller_id(default"@external"if no identity) plus a redactedidentitysnapshot whencontext.identityis set. Sensitive identity attrs (bearer_token,secret, etc.) are replaced with"<redacted>"via the existing_audit_payload_extrashelper.Registry.register()/Registry.unregister()accept an optionalcontext=kwarg used solely to enrich the audit payload.4. Soft warning for missing
requires_approvallogging.warning(...)fires when anephemeral.*module is registered withoutrequires_approval=True, citing the RFC and recommending the caller set it.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)
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 checkclean.Files changed
src/apcore/module.py—discoverablefield onModuleAnnotations.src/apcore/registry/registry.py— namespace reservation, audit emit, soft warning,discoverablefilter,set_event_emitter(),EPHEMERAL_NAMESPACE_PREFIXexport.tests/registry/test_ephemeral_modules.py— new test suite.tests/test_conformance.py— pilot-tolerant normalisation: stripsdiscoverablefrom 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]→### Addedwith 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:
_bridge_registry_eventshelper inapcore.sys_modules.registrationalready emits empty-payloadapcore.registry.*events for all registrations via the callback bridge. The pilot adds a separate registry-side direct emit forephemeral.*only, with the contextual payload. When bothregister_sys_modules()andset_event_emitter()are wired, an ephemeral.* registration produces two events with the sameevent_type(one with emptydata, 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.discoverableinteraction withregister_internal().register_internal()callsmerge_module_metadatadirectly, so it honoursdiscoverable=Falseset in code. The_is_discoverablehelper falls back to the module instance'sannotationsattribute when_module_meta["annotations"]is absent, so both registration paths behave consistently.annotations_extra_round_tripdoes not yet includediscoverable. The Python test was made pilot-tolerant; the canonical fixture underapcore/conformance/fixtures/will need a follow-up update once the RFC is accepted.🤖 Generated with Claude Code