All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Ships
PROTOCOL_SPECv1.34.0 → v1.36.0. The three spec versions are one release: v1.34.0 declared the path-typed key set, v1.35.0 built the binding-discovery and path-resolution contracts on it, and v1.36.0 repaired what writing the first conformance drivers for those contracts exposed. Each spec version keeps its own row in §13's revision table.
-
PROTOCOL_SPEC§9.2.1 Path-Typed Configuration Keys (new, spec v1.34.0) — the specification never said which configuration values are filesystem paths, so every consumer that needed to know maintained its own list (#113). §9.1.1 gives four keys a relative default (extensions.root,schema.root,acl.root,bindings.dir) and §9.2 makes each of them environment-overridable, but nothing marked them as paths. That answer matters outside this repository: a consumer forwarding apcore configuration across a process boundary — a CLI spawning a worker, a supervisor building a container environment — has to know whichAPCORE_*variables carry paths, because a relative value silently re-roots wherever the working directory differs.apcore-clihad already hand-maintained exactly such a list (SANDBOX_PATH_TYPED_VARS), carrying a note that adding a path-typed key means editing three SDKs; nothing could detect drift between that list and this repository. The set is now closed and declared where the key surface is already defined:schemas/apcore-config.schema.jsonandschemas/defaults.schema.jsoncarry"x-apcore-path": trueon each path-valued property, implementations MUST expose the set through a public accessor, and a path-valued key added to §9.1 MUST carry the marker in the same change. Two exclusions are stated rather than left to inference, because both are the mistakes an implementer would otherwise make:bindings.patternis a glob matched against filenames withinbindings.dirand is never resolved as a path itself, andextensions.rootsis list-valued with no scalar environment encoding — an implementation MUST NOT invent a delimiter-separatedAPCORE_EXTENSIONS_ROOTS. Purely additive: no key changes meaning, no default moves, and no SDK behaviour is required to change beyond exposing the accessor. The resolution base is deliberately not decided here.acl.rootresolves against the config file's directory (ACL.discover, D-64) whileschema.rootresolves against the process CWD (SchemaLoader) — a live divergence between two sibling keys, tracked in #113, whose repair is a separate decision between a CWD-for-everything rule (MINOR) and origin-tracked resolution (MAJOR, with the two-minor deprecation §13.2 requires). Declaring the set is the prerequisite both options share, which is why it lands first and alone. New conformance fixtureconformance/fixtures/config_path_typed_keys.json(7 cases), whose discriminating case isbindings.pattern: an implementation that classifies path-typed keys by config section, or by "the default looks like a filename", passes every presence-only assertion and fails that one. -
§9.2.1 requirement 5 — an empty string is not a path. §9.2 treats a set but empty
APCORE_*variable as an override, soexport APCORE_ACL_ROOT=silently blanks a directory the configuration file correctly declared, and the resulting""then resolves to the working directory. The same shape is already on record forAPCORE_CONFIG_FILE(#88), where an empty value injected a phantomconfig.filekey; path-typed keys are the population where it fails silently rather than loudly, because""is a legal relative path to every filesystem API and never the one an operator meant. An empty path-typed value MUST be discarded and resolution MUST fall through to the next tier. Found by the TypeScript conformance work, which hit it on a real test run. -
§9.2.2 fixes the deprecation warning's cadence: once per configuration load, never once per process. v1.35.0 required the warning and stated its two narrowing conditions but said nothing about cadence — and the three SDKs immediately invented three different ones (Python deduplicating through the
warningsfilter, TypeScript holding a module-global once-flag, Rust warning per load). A process-global flag makes emission order-dependent: whichever configuration loads first consumes the warning, so a later affected document is silent and the operator cannot tell which one triggered it; it is also a test-isolation hazard, which is how the divergence arose. De-duplication for log volume belongs to the host logging layer. This is the same "specification is silent, so each implementation invents an answer" pattern the §9.2.x work exists to close, caught inside the change that closes it. -
schemas/defaults.schema.jsongains thebindingssection it never had.bindings.dir(./bindings) andbindings.pattern(*.binding.yaml) were declared in §9.1.1 and inapcore-config.schema.json'sBindingsConfig, but absent from the file that describes itself as the "Single source of truth for all configuration default values across apcore SDK implementations. SDKs MUST use these values as fallbacks." Becauseconformance/fixtures/config_key_governance.jsonpins each SDK's default table to that schema, no SDK could carry the default — all three had to hardcode./bindingsat the loader instead, which is the same declared-in-one-place-unreachable-from-the-mechanism defect as the rest of this batch.bindings.dircarriesx-apcore-path: truelike its three sibling roots.config_key_governance.jsonregenerated: 65 allowed keys, 20 canonical defaults. This requires a matching change in all three SDK default tables, or the governance fixture fails. -
PROTOCOL_SPEC§9.2.2 Path Resolution Base (new, spec v1.35.0) — §9.1.1 gives four keys a relative default and nothing said what they are relative to (#113). v1.34.0 closed the question of which configuration values are paths and deliberately left open what a relative one resolves against. That base is not merely undocumented — it is two different bases in the shipped SDKs:acl.rootresolves against the configuration file's directory (ACL.discover, decision D-64,docs/features/acl-system.md) whileschema.rootandextensions.rootresolve against the process CWD, unconditionally. Two sibling keys, identical relative values, identicalAPCORE_*override syntax, different answers, and neither behaviour violated anything, because the specification had no opinion. The project root is declared. EveryConfighas exactly one, fixed at load time: the directory containing the configuration file when that file was selected by §9.14 discovery tiers 1-5 ($APCORE_CONFIG_FILE, or a project-local./project.yaml/.yml/./apcore.yaml/.yml), and the process CWD when it came from the user-level tiers 6-7 (~/.config/apcore/config.yaml,~/.apcore/config.yaml), when discovery found nothing, or when theConfighas no backing file. The discovery tier is what selects the base, and it has to be: in tiers 2-5 the file's directory is CWD and every candidate rule agrees, so that population — the overwhelming majority — is unaffected either way; tier 1 is where a configuration file can sit outside CWD, and there its own directory is the better reading of a path written beside it; tiers 6-7 are where the file's directory is wrong, becauseextensions.root: ./extensionsin~/.config/apcore/config.yamlcannot mean~/.config/apcore/extensions. A user-level configuration's relative paths are per-project by intent. The user-level tier is where today's behaviour is actively wrong, and it isacl.root— the security key — that has it. A config at~/.config/apcore/config.yamlcarryingacl.root: ./aclloads its policy from~/.config/apcore/acl/into every project that user runs, while the project's own./acl/is ignored: for a default-deny, explicitly-granted authorization system that is the inverse of the intent. The same load resolvesextensions.rootagainst CWD, so one configuration document produces two bases in a single load — reproduced, not hypothesised, with the §9.3 semantic check firing about the project's missing./extensionsin the same run that silently loaded a foreign ACL. Target semantics (v2.0). Every relative path-typed value — §9.2.1's closed set — resolves against the project root: file-declared, environment-sourced, API-supplied and the §9.1.1 defaults alike. Three properties are stated as requirements rather than left to implementers: one base perConfig(an implementation MUST NOT resolve two path-typed keys of the sameConfigdifferently), no per-key origin tracking (nothing has to record which precedence tier produced a value — the largest implementation cost across three SDKs, removed by construction), and defaults have the same home as everything else ("./schemas"resolves like a written value).APCORE_ACL_ROOT=./xset for a project run resolving against the project root is also the better reading of intent: an operator setting it means "relative to this project". This release changes no behaviour. It is the deprecation phase, which §13.2's "keep at least 2 minor versions for deprecation period" makes the floor for a change to deployed configurations. The 1.x line keeps the current semantics exactly, and adopting the target rule early is forbidden by requirement 3. What is required now is additive: implementations MUST expose the project root through a public accessor (Config.project_root/projectRoot()/Config::project_root()), carrying no resolution behaviour, so an application, a CLI or a conformance driver can ask what the base will be before anything depends on it. Implementations SHOULD warn — but only when the project root differs from CWD and at least one path-typed value is relative. A blanket warning is explicitly rejected: it would fire for every project in tiers 2-5, where nothing changes, which trains operators to ignore the one warning that matters. Migration, by tier: tiers 2-5 — none,project_rootalready equals CWD. Tiers 6-7 —acl.rootmoves from the user-level directory to CWD, which is a bug fix, not a regression; the other three keys already resolve against CWD and do not move. Tier 1 with a config outside CWD — the one genuine break, whereschema.root,extensions.rootandbindings.dirmove from CWD to the configuration file's directory. That is precisely the population the narrow warning is scoped to reach. Side effect on a Proposed RFC.docs/spec/rfc-config-include.mdopen question #1 — whether a path value declared inside an included fragment resolves relative to that fragment or to the root file — is settled by adoption: one base for the wholeConfigleaves the fragment-relative reading no room, since it would reintroduce exactly the per-value origin tracking §9.2.2 forbids. That RFC's Status remains Proposed; this section constrains the answer it may give and does not adopt it. New conformance fixtureconformance/fixtures/config_project_root.json(14 cases), carrying one case per §9.14 tier because the tier is the input under test, plus the three cases that pin the warning condition from both sides (fires when root ≠ CWD and a relative value is present; silent when the roots coincide; silent when every path value is absolute).v1x_current_bases_unchangedpins the deprecation phase itself: under a tier-1 config outside CWD,acl.rootstill resolves file-relative andschema.rootstill resolves against CWD, withacl/andschemas/present under both directories so exactly one semantics passes — an SDK that ships the v2.0 rule ahead of its window fails there.docs/features/acl-system.md'sACL.discoverSide Effects step 2 is annotated as current-behaviour-superseded-at-2.0; its described behaviour is unchanged and remains what implementations must ship for the whole 1.x line.
-
Fixture repairs, all found by writing the first conformance drivers for v1.35.0's own fixtures. These fixtures were authored from the specification text and had never been executed by any implementation; the drivers were the first thing to run them, and three SDKs reported the same defects independently.
bindings_dir_resolutiongainsenv_var_must_not_be_read_directly_at_the_loader— §5.12.6 clause 2 had no case at all, so an implementation reading the rawAPCORE_BINDINGS_DIR(precisely the apcore-typescript#36 defect the clause exists to forbid) passed the entire fixture; the new case sets the variable after theConfigis built, which separates a loader reading the merged configuration from one reading the environment. Its candidate directories now carry distinct module ids: with one shared id,env_overrides_config_file_dirreported["greet"]whichever directory was scanned, so the case that exists to pin env-over-file precedence could not detect an implementation ignoring the env tier.config_project_root's tier-6 and tier-7 cases now name the tier through<tier6_config>/<tier7_config>tokens instead of hardcodingfakehome/.config/apcore— §9.14 states tier 6 is platform-varying (XDG on Linux,~/Library/Application Supporton macOS), so the literal spelling made every driver fail on macOS while asserting nothing extra on Linux.no_warning_when_all_path_values_absolutenow spells every §9.2.1 key absolutely; as first published it set onlyschema.rootandacl.root, leavingextensions.rootandbindings.dirat their relative §9.1.1 defaults, which §9.2.2 counts — so the case was unsatisfiable against the very rule it was testing, and all three SDKs reported it as such. -
PROTOCOL_SPEC§5.12.6's binding-discovery MUST had no subject and no trigger, and no SDK satisfied it (spec v1.35.0, #114). The requirement read "ifbindings.diris configured, implementations MUST scan files matchingpatternin that directory". It never said who scans or when, and that absence is why both obvious readings felt wrong — each was a different guess at the missing subject. Measured rather than assumed:bindings.diris registered in all three key surfaces (apcore-python config.py:217,apcore-typescript config-key-surface.ts:70,apcore-rust config.rs:213) and read by no code path, whileBindingLoaderis exported public API in all three (__init__.py:198,index.ts:277,lib.rs:66) and called from no internal one. That is a design, not an oversight: binding loading is a user-invoked tool. A user who setbindings.dirinapcore.yamlgot no scan in any SDK; a user who exportedAPCORE_BINDINGS_DIRgot one in TypeScript only, which had implemented the environment tier alone of the key's precedence chain via a rawprocess.envread atbindings.ts:163. The requirement now names its subject and its trigger. A binding loader invoked without an explicit directory argument MUST resolve the scan directory frombindings.dirunder §9.2 precedence —APCORE_BINDINGS_DIR> configuration file > default"./bindings"— and MUST match candidate files againstbindings.patternthrough the same chain, default"*.binding.yaml"(the pattern moves out of the loader signature, where it had been living as a hard-coded default). An explicit directory argument still wins: explicit > env > file > default. And an implementation MUST NOT readAPCORE_BINDINGS_DIRdirectly at the loader — the environment tier arrives through the ordinaryAPCORE_*override mechanism, so one precedence chain governs the key and TypeScript's existing users keep working. Auto-scanning at initialisation is now forbidden explicitly, because it is the reading the old wording invited: implementations MUST NOT scan a binding directory as part of client or framework initialisation. It would add filesystem I/O to every client's startup and change behaviour for every deployment that merely happens to have a./bindingsdirectory. Nothing has asked for it. The MUST is not weakened — a requirement no implementation could satisfy, and whose subject could not be identified from its own text, is made enforceable and testable for the first time. Nor is this a re-run of thebindings.fileswithdrawal recorded in the same section: that key was schema-invalid (BindingsConfigisadditionalProperties: falseover{dir, pattern}), absent from every key surface, and implemented nowhere, so there was nothing to wire.bindings.diris declared by the canonical schema with a default, present in all three key surfaces and inconfig_key_governance.json, and already half-implemented in TypeScript — withdrawing it would mean a §13.2 deprecation cycle across the schema, three key surfaces and the governance fixture, more churn than the fix, and it would break existing TypeScript users. That note is retained unchanged. New conformance fixtureconformance/fixtures/bindings_dir_resolution.json(8 cases). Its discriminating case isconfig_file_dir_is_scanned_with_env_unset—bindings.dirin a config file,APCORE_BINDINGS_DIRunset, loader called with the directory argument genuinely absent — because every existing loader test in all three SDKs passes an explicit directory, which is the one path that works identically under both the old and the corrected behaviour.no_auto_scan_at_initpins the prohibition from the other side by placing a well-formed binding file under the configured directory and asserting its module ID never reaches the registry.
-
PROTOCOL_SPEC§5.12.2 declared the binding fieldtarget_ida MUST; the canonical schema, the companion spec, both binding fixtures and all three SDKs usetarget(#115). The protocol specification was the sole outlier, and it was the outlier on a MUST, in the section whose entire purpose is to define the binding-file format — so a binding file written from the spec loaded in no SDK. Corrected throughout §5.12, and in the ten further occurrences outside it (§5.13.9'sResolvedModuletype, §8.2 / §8.6 / §8.7 error descriptions, §5.14.6 and §9.15 prose) where a past over-appliedtarget→target_idrename had left the wrong spelling standing, including in ordinary English ("Deploy application to target_id environment"). The 16 remainingtarget_idoccurrences are ACLcaller_id/target_idand schema$refcontexts, which are correct. No implementation changes and no deployed file is affected: the population of binding files usingtarget_idis empty, because such a file has never loaded.conformance/fixtures/bindings_dir_resolution.jsoncorrected to match. -
§5.12.6 never said what happens when the resolved binding directory does not exist, and the fixture guessed the opposite of every implementation. v1.35.0 gave that section's MUST a subject and a trigger but left its failure mode unstated. New clause 5: a resolved directory that does not exist MUST raise, naming the resolved directory, and MUST NOT return an empty result — which is what apcore-python, apcore-typescript and apcore-rust all already do, each with tests pinning it. The contrast with
ACL.discover's missing-path no-op (D-64) is now recorded as deliberate rather than accidental: ACL discovery is automatic, so a missingacl.rootmust be silent; binding loading is user-invoked, so an absent directory is a mistake, and returning zero modules silently reproduces the "configuration key that quietly does nothing" defect §5.12.6 was rewritten to remove.
ApprovalRequestgainscaller_idandaction(spec v1.32.0, decision D-03).docs/features/approval-system.md's Contract block has required both since the Contract blocks were added; the 2026-05-02 alignment review recorded the same requirement as D-03, and neither field had reacheddocs/spec/protocol-spec.md§7.3.1's own YAML schema — the single source of truth for the type was the one place that had not caught up.caller_idis nullable (null on a top-level call, mirroringContext.caller_id);actionis a flat duplicate ofmodule_id, populated from the same construction site every SDK'sBuiltinApprovalGatealready has. Additive — no existing construction call breaks. New conformance fixtureconformance/fixtures/approval_request_fields.json.- Canonical
## Contract:blocks added for public APIs that had none, found by a documentation-completeness audit.CancelToken.is_cancelled/.check/.reset(cancellation.md;.checkwas previously documented under the nameraise_if_cancelled, which no SDK exposes — retitled to match the real API);ApprovalHandler.check_approval(approval-system.md);Executor.register_strategy,.list_strategies,.governance_state(core-executor.md); the Rust-onlyAPCore.with_components/.with_options/.reload(apcore-client.md); the six read-onlysystem.health.*/system.manifest.*/system.usage.*modules andOverridesStore.load/.save(system-modules.md); andTraceContext.inject/.extract/.from_traceparent,BatchSpanProcessor.on_end/.force_flush/.shutdown,ErrorHistory.record/.get/.get_all,UsageCollector.record/.get_summary/.get_module/.get_latencies,UsageExporter.export/.shutdown, andPeriodicUsageExporter.start/.stop(observability.md). Documentation-only; no SDK behaviour affected.core-executor.md's three sections headed## Contract:but lacking Inputs/Errors/Returns/Properties (Executor binding to Context,Distributed cancellation,`global_deadline` distributed semantics) were brought to canonical shape — the first gained the missing structure (it has a real cross-boundary member,bind_executor/withExecutor, behind it); the other two, which describe cross-cutting rules rather than a single symbol, were retitled without the misleadingContract:prefix.middleware-system.md'sStepMiddlewarecontracts were promoted from non-canonical### Contract:/#### Inputsto## Contract:/### Inputs, matching every other contract in the file.
docs/features/apcore-client.md'sAPCore.validatecontract contradicted itself on a malformedmodule_id, and the half no SDK implements was the one readers would act on. The Errors block carried both "no errors are raised for validation failures" and "InvalidInputError(code=INVALID_MODULE_ID)— raised ifmodule_idis empty or malformed (before pipeline begins)". The second contradicted the same contract's Returns block, which listsmodule_idas the first preflight check — a check that can only report a result if the malformed case reaches it instead of raising past it. apcore-python is unambiguous:Executor._validate_asynccatches theInvalidInputErrorfrom_validate_module_idand returnsPreflightResult(valid=False, checks=[…]). The raise clause is removed, with a note recording the deliberate contrast againstExecutor.call, where the identical input does raise at the entry guard:call()executes and must refuse,validate()reports and must not.- The Rust adaptation table said
module()was unavailable in Rust; apcore-rust has hadAPCore::moduleall along. The row read "N/A — Rust has no decorators; useimpl Module+register()" — true about decorators, false about the method:client.rsexposesmodule(module_id, description, input_schema, output_schema, documentation, tags, version, metadata, examples, display, handler), building and registering aFunctionModulein one call as the Python/TypeScript helper does. Corrected, andimpl Module+register()is now described as what it is: the route for a module needing more than a handler closure. - The same table did not record that Rust's
APCoreconstruction has nopolicyinput. Python'sAPCore(policy=…)and TypeScript'snew APCore({ policy })accept anExecutionPolicy; Rust'swith_optionsdoes not. A genuine surface gap, but not a capability gap — both other SDKs ignore their ownpolicyargument when the caller supplies an executor and direct them to wire it on that executor instead, which is exactly the route Rust callers already take viawith_options(None, Some(executor), …). Documented as an adaptation rather than closed by widening the Rust constructor. docs/features/module-interface.mddocumentedModule.previewand Rust-onlyModule.as_streamingas one-line table rows with no canonical contracts. Both now have## Contract:blocks.preview's states the constraint that is the entire point of the hook — it MUST NOT perform the side effectsexecute()would — which the table row only implied.as_streaming's states theSome/Noneconsistency requirement againstModule::stream()as a normative clause rather than a parenthetical.docs/features/apcore-client.md's method table claimedAPCore.reload()"re-discovers modules"; apcore-rust's own doc comment says the opposite ("Module re-discovery is not triggered; callAPCore::discoverexplicitly afterreloadif you need fresh module discovery"). Corrected the table entry against the actualapcore-rustsource and added## Contract: APCore.reload(ModuleError(code=RELOAD_FAILED)when the client'sConfighas no backing file path;ModuleError(code=MODULE_RELOAD_CONFLICT)on a concurrent-mutation race), alongside new contracts for the other two Rust-only constructors,with_componentsandwith_options. Documentation-only.docs/features/observability.md's W3C Alignment Rules Rust example calledTraceContext::inject(&context, Some(parent_id))andTraceContext::inject_checked(&context, Some(parent_id))— neither signature exists inapcore-rust.injecttakes onlycontext; theparent_id/trace_flags/tracestateoverrides live oninject_with_options(and the validatinginject_checked, which takes the same four arguments). Corrected the example to compile against the real API, and the newTraceContext.injectcontract documents the resulting cross-language gap: Rust'sinject/inject_with_optionssilently fall back to a randomparent_idon a malformed override instead of raising, unlike Python/TypeScript, andTraceContext.from_traceparenthas no Rust equivalent at all.docs/spec/protocol-spec.md's own version header lagged its content. The frontmatterdescriptionstill readv1.28.0and theVersion:/Last Updated:lines still read1.30.0/2026-08-31, three releases behind the changelog table already recorded in the same file (up to1.31.0, #112). Synced all three to1.31.0/2026-09-04. Metadata-only; no normative text changed.docs/features/apcore-client.mddocumentedAPCore.start/APCore.stopcontracts that no SDK implements, contradicting decision D-09 (2026-05-02 alignment review, recorded indocs/spec/2026-05-decision-log.md), which found the two methods aspirational with no clients and called for their removal — already carried out inPROTOCOL_SPEC§12 but missed in this feature spec. Removed both## Contract:sections and added aclose()note to the Module Lifecycle requirements documenting it as a Python-only convenience (releases the cached sync event loop; idempotent; no TypeScript/Rust equivalent), per D-09's action item.AsyncTaskManager.start_reaper's Contract required the call itself to be awaited, in all three SDKs, though none of them make it genuinely awaitable and none needs to (docs/spec/2026-09-decision-log.mdE-01). Decision D-11 fixed the argument names, units, andReaperHandlereturn type; it never decidedstart_reaperitself must be async, and no implementation has ever had anawaitinside it before returning the handle — starting a background sweep loop is a synchronous action in Python, TypeScript, and Rust alike.docs/features/async-tasks.md's signature table, Properties block, and code examples corrected:start_reaperreturns the handle directly in every SDK; onlyReaperHandle.stop()is genuinely awaitable. No SDK behaviour change — Python and Rust were already conforming to the corrected text; TypeScript'sPromise<ReaperHandle>return (from this session's separate SDK dispatch) is documented as a compatibility surface, not elevated to a cross-language MUST.APCore.discover's Properties line claimed "synchronous in all languages," contradicting both TypeScript and Rust (docs/spec/2026-09-decision-log.mdE-02). TypeScript's default discovery path resolves each module's entry point via ESM dynamicimport(), which has no synchronous form in Node; Rust'sRegistry::discoverawaits aDiscoverertrait that isasyncby its own definition, mirroring TypeScript'sCustomDiscovererand Python'sApprovalHandleras a pluggable, possibly-async extension point.docs/features/apcore-client.mdcorrected to state Python sync / TypeScript+Rust async, each with its structural reason, and to name the return-value/error/registry-state contract as what actually holds across languages. No SDK behaviour change — documentation-only.- apcore-rust's
APCore.on/offcould not report that the client has no event bus, so a subscription on a misconfigured client silently went nowhere.on()calledget_or_insert_withon itsevent_emitterfield, creating a standalone bus whensys_moduleswas disabled: the call returned a subscriber ID, and no framework event ever arrived, because the sys-modules and registry emitters are a different bus.off()mirrored it, returning a barefalsethat reads as "no such subscriber" when the real answer was "there is no bus". apcore-python and apcore-typescript both raiseSysModulesDisabledErrorhere, and this page's own Error Behavior table has required it throughout — apcore-rust's conformance suite even carried the two cases pre-written under#[ignore]attributes naming the gap. The Rust adaptation table is updated for the resulting signatures:on()now returnsResult<String, ModuleError>andoff()Result<bool, ModuleError>, theboolretained (Python/TypeScript return void) because "no such subscriber" and "events are off" are different answers and collapsing the first into the second is what made the defect invisible. This IS an SDK change in apcore-rust only, and a breaking one — see that repo's0.29.0BREAKING entry for the migration. ACL(rules=[...])'s own construction door did not re-validate a rule mutated before it was ever passed in (spec v1.33.0,docs/spec/2026-09-decision-log.mdE-03). apcore-typescript and apcore-rust already rejected the sequence "construct a well-formed rule, mutate it, then hand it straight toACL(rules=[rule])for the first time"; apcore-python silently accepted it, deferring safety to the §6.1.1 UNEVALUABLE backstop atcheck()time.ACL's own constructor is one of the three entry points §6.1.6 rule 3 names, and PROTOCOL_SPEC's own "assigned onto an already-constructed rule" language was ambiguous between "a rule already installed inside a live ACL and mutated afterward" (the genuinely uninterceptable backstop case) and "any rule object currently holding a bad value" — apcore-typescript and apcore-rust read it the first way, apcore-python the second. §6.1.4.1 and §6.2.1 are corrected to name the narrower, correct boundary;ACL.__init__(apcore-python) now validates every rule it is handed, matchingadd_rule. New conformance driver case (_door_construct_mutated) added totest_acl_pattern_arity.py; the existing 9installed_rulebackstop cases are unaffected. This IS an SDK change in apcore-python only.
-
A pattern list with no operands made an ACL rule inert, and under
default_effect: allowthat permitted the call the rule named (spec v1.31.0, #112).callers/targetsof[],["$or"]or["$not"]can never match; all three SDKs returnedfalsefrom the matcher andvalidate_rules()reported nothing, so adenyrule an operator wrote, loaded and validated contributed nothing to the decision. Reached from a plain YAML file —ACL.loadrejects an omittedcallers/targetsand permits an empty one.The array's shape is now closed at every entry point (§6.2.1): at least one element, every element a non-empty string,
$orwith at least one operand,$notwith exactly one, and$or/$notnowhere but index 0 — rejected withACLRuleErrorat file loading, direct construction and runtime insertion alike.schemas/acl-config.schema.jsonhad declaredminItems: 1andminLength: 1on both fields since the file existed, enforced by nothing: the same shape as #107 and #111, where the constraint was in the schema and no door enforced it, because no implementation validates an ACL file against the schema at load time.Three normative statements are replaced, not reinterpreted. §6.5's edge-case table required an empty list to make the rule "never match". §6.2.1 required
["$not"]to "evaluate to false (fail-closed)" — a label that predates §6.1.1 (v1.22.0) and is wrong, because a non-match is fail-closed on anallowrule and fail-open on adenyone. And["$not", p1, p2, …]was implementation-defined: consultp1, drop the rest, which every SDK did, sotargets: ["$not", "secrets.a", "secrets.b"]on anallowrule grantedsecrets.b— the second target the operator excluded.§6.2.1 also says for the first time that a pattern array is flat — the operators do not nest and there is no precedence, unlike the same two tokens inside
conditions— and gains the worked examples it never had. A reserved token away from index 0 is now rejected, which makes the section's own long-unenforced "MUST NOT match a literal module ID equal to$or" hold by construction; measured beforehand, all three SDKs matched a module literally named$not.A second, validator-only tier reports arrays that are well-formed and still match nothing —
["$not", "*"]has legal arity, exactly one operand, and matches nothing, producing the identical fail-open. Those keep loading and change no decision. Stated as a criterion with a MUST-detect minimum rather than an enumeration, because the predicate cannot be closed without freezing the pattern language, and an incomplete predicate at a door would mean the same ACL file loads in one language and fails in another.Two orderings are pinned because implementing this in three SDKs produced two answers to each:
add_ruleMUST re-validate the rule it is handed, including one mutated after construction (a closedeffectis never read again, a pattern array is); and validation order iseffect→approval→callers/targetswith rule index dominating all three, the pattern fields counting as one axis in which the §6.1.4.1 type fault precedes the shape closure andcallersprecedestargets. Three implementations produced three different axis orders, and one produced two different answers through two of its own doors becauseloadvalidated rule by rule while direct construction swept axis by axis. §6.1.6 was cited for this ordering in an early draft and states none; §6.2.1 states it for the first time.BREAKING for any deployment carrying one of these shapes — which is exactly the population that believes it has a rule and does not.
targets: []meaning "everything" becomes["*"]; meaning "nothing" means the rule should be deleted. The multi-operand$notis the one shape with no mechanical migration:["$not", p1]preserves what the rule has actually been doing, but ifNOT (p1 OR p2)was intended, a leadingdenyis not equivalent — a non-matching rule lets evaluation continue to later rules and adenyends it — so that rewrite has to be done by hand against the rule's position. Migration tooling MUST NOT apply it automatically.New conformance fixture
conformance/fixtures/acl_pattern_arity.json(41 cases).acl_evaluation.jsondropsempty_callers_matches_noneandempty_targets_matches_none, which asserted the replaced reading.
-
The ACL could refuse on a call's arguments, and could not ask about them (spec v1.28.0, #108).
git pushandgit push --forceare one module, and the only way to gate the dangerous half was to mark the whole modulerequires_approvaland stand down inside the handler — which floods the audit trail with approvals for calls nobody needed to see and weakens the annotation from "this needs approval" to "this might". Every decision point that can read a call's arguments was unable to escalate it to a human, and the one point that decides whether to ask a human (§7.9's policy resolution) is forbidden by §7.9.6 rule 2 from consulting them.§6.1.6 — authorization and approval are two results, not one. A rule keeps
effectand gains an orthogonal optionalapproval: required | not_required. Absence meansnot_required, so every rule written before this section keeps its meaning exactly.approval: requiredon adenyrule is rejected at every entry point that accepts a rule — file loading, direct construction and runtime insertion: "denied and needs approval" is not a state that means anything, and adenyrule never reaches the approval gate, so a requirement attached to one can only mislead the operator who wrote it.add_rulereturns nothing by its own contract in all three SDKs, which is not an exemption — an implementation provides a fallible variant beside it or fails loudly. Adding the field was only safe once §6.1.5 closed the rule key set in v1.27.0; an SDK that still dropped unknown keys would read adeny-with-approvalrule as a bare rule and act on half of what the operator wrote.§6.1.7 — the built-in
argumentscondition, with three structure-only predicates:has_key,has_all_keys,has_none_of. No predicate reads a value, and that is a constraint rather than a first cut. The argument view at Step 4 is not reliably redacted — redaction is driven byx-sensitivemarkers in the module's input schema, and a module without one gets no field redaction at all — so a value-reading predicate would pull secret-bearing data into the governance decision path and intohandler_errordiagnostics. The arguments are also unvalidated at that point, because the ACL check is Step 4 and input schema validation is Step 7. Key presence is the one question well-defined on unvalidated input, and it answers the driving requirement: "did this call carry--force?" It is built-in and needs no registration, because a deployment-registered argument handler would be exactly the unauditable host code §7.9.6 rule 2 keeps out of a governance verdict.§6.1.8 — the governance projection. The condition reads a projection carrying the argument key set and optionally each key's JSON type, never a value: a projection that structurally cannot hold a value cannot leak one, whatever a future predicate does with it. It is computed by the framework at Step 3 and MUST NOT be accepted from caller-supplied input — a caller that could supply its own could satisfy
has_none_offor a call whose arguments say otherwise, turning the condition into a caller-controlled switch. Substitutingredacted_inputsis forbidden: its contract is safe logging, and one field serving both that and "input to a security decision" will eventually break one of them in a change made for the other. Four shapes resolve to UNEVALUABLE rather than to a boolean — no projection available, an empty predicate object, an unrecognised predicate name, a malformed predicate value — because each is a case where "false" is safe on adenyrule and fails open on anallowrule. The first is the sharp one:has_none_ofover an empty stand-in is satisfied, so anallowrule would grant for a call whose arguments were never seen.§6.8.1 and §6.3.1. The structured accessor returns
accessand the approval requirement separately; the legacy boolean fails closed on an approval requirement, because a boolean can only be read as "let it through" and letting it through would run a call the ACL said needed a human.AuditEntrygainsapproval_requiredbesidedecisionrather than widening it —decisionis a string downstream consumers parse, and a third value would break every existing parser.Landed with
conformance/fixtures/acl_argument_scoped_approval.json(20 cases) and drivers in all three SDKs. Writing the fixture is what settled the last two questions: anargumentsfault's condition path descends toarguments.<predicate>, and every faulty predicate in one block is reported rather than the first — apcore-rust already did both, and the divergence between the other two was measured, not hypothesised.
-
Four corrections to v1.22.0–v1.24.0, found by implementing them in all three SDKs (spec v1.25.0, #100 / #102). The first round of SDK work was run as three independent implementations against one spec text, and the places where they diverged are where the text was underspecified.
§6.1.1's closed list of three unevaluable situations was wrong, in the direction the section exists to prevent. All three SDKs classified a malformed compound value (
$or: "not-a-list",$not: 3) as UNSATISFIED, and all three independently flagged the choice as suspect — a handler handed a malformed value does run to completion, so the closed list left no other reading. The result was adenyrule carrying$or: "typo"staying inert: the v1.22.0 defect, through a door v1.22.0 left open. A non-mappingconditionsproduced a three-way split on identical input — apcore-python raisedAttributeErrorout ofcheck()(violating its own contract thatcheckMUST NOT raise), apcore-typescript denied, apcore-rust went inert. "Unevaluable" is now a principle — the implementation cannot answer the condition as written — with five non-exhaustive examples, and an implementation meeting an unlisted case MUST classify it by the principle rather than defaulting to UNSATISFIED.§6.1.4 (new) — a context-independent structural and registry precheck, which settles two questions that were pulling against each other. §6.1.1 rule 2 wanted deterministic
handler_errorwhile the composition rules permitted short-circuiting; and §6.5 kept "no context supplied" a non-match, which let a misspelled key on a context-less call escape §6.1.1 entirely — verified in all three SDKs, all three returned "allowed". The precheck walks the whole rule structure without a context and without running a handler, and runs before §6.5's context check. A rule that passes the precheck and then finds no context still takes §6.5's path, so a registered, context-dependent condition such asrolesis not unevaluable merely because this caller sent no identity — §6.5's design is intact and only the malformed case changes. Because the precheck is exhaustive and handler-free its findings are a pure function of the rule, so precheck-origin diagnostics are identical across implementations while execution-origin ones may still vary with short-circuiting. That is stated explicitly rather than left to aSHOULD NOTthat pinned neither behaviour.Condition paths.
handler_errorand the validator now order by path ($or[1].$not.k) rather than by key, because a nested$ormay carry one key at several positions, which leaves ordering by key undefined.§6.1.3 —
sync_registered/async_registeredrenamedsync_resolvable/async_resolvable. They always meant "resolvable on that evaluation path", and sinceasync_check()falls back to the sync registry,async_resolvableis the union of both — so the old name read as a registry lookup and would be false for every built-in leaf handler, which resolves on both paths.§7.9.6 — rule 3(b) withdrawn. It promised that "a host-supplied policy implementation can decide on arguments".
ExecutionPolicyis a concrete class in apcore-python and apcore-typescript and a concretestructin apcore-rust, andset_policytakes that concrete type in all three, so no host can supply one; the clause described a capability that does not exist. Making it pluggable is deliberately not specified — no requirement has asked for it. Rule 5 (now 7) is restated as a capability requirement rather than an API shape after the three SDKs produced three reasonable shapes (an added method, keyword-only parameters, an options object); and_approval_tokenmust now be stripped before policy resolution, which §7.4's existing "before passing to subsequent steps" does not reach, since resolution happens inside Step 5.§6.8 clarifies rather than relaxes: an accessor MAY acquire the ACL lock internally, copy, and release before returning — what is forbidden is returning a value whose validity depends on a lock the caller must release. §6.3.1 records that
handler_erroris per-check()whilematched_rule_indexis per-rule, so one audit entry may legitimately describe two different rules.
-
A rule's
effectaccepted any string outside the YAML loader, and was silently read asdeny(spec v1.30.0, #111). §6.1's field table sayseffectMUST beallow | denyandschemas/acl-config.schema.jsondeclares the enum, but nothing enforced it away from the file path. This is #107 one level down: there the rule key set was closed while an unknown key was dropped in silence; here the key is legal and its value is dropped, in the same silence.Measured on
effect: "Allow"— a capitalisation an operator writes by hand. apcore-python and apcore-typescript rejected it fromACL.load()and accepted it through direct construction andadd_rule(). apcore-rust rejected it atloadand at construction and accepted it atadd_rule(), whose validation covered §6.1.6'sdeny+approvalcombination and nothing else. All three had a hole and merely had different ones — which is why the closure is specified per entry point rather than per implementation. All three emit the identical loader message "Rule 0 has invalid effect 'Allow', must be 'allow' or 'deny'", so the check existed everywhere and was simply not reached from every door.The inconsistency was internal too. All three validate
default_effect— the same two legal values one field up — at every door that accepts one. So the loader guarded one door,default_effectguarded all of them, and a rule'seffectguarded only the file path. §6.1.6 rule 3 already requires rejection at every entry point that accepts a rule, and had never been applied to the field it is named after.Not a privilege escalation, because no unknown value grants. It is a silent functional break in two distinct shapes. Where the value is normalised toward
deny, a rule the operator wrote to permit denies everything it matches underdefault_effect: allow, with no error, no warning, and nothing fromvalidate_rules(); on adenyrule that reading is only accidentally right, true until someone revisits which way it points. Where the value is not inspected at all — apcore-rust — the raw string reachedAccessDecision.accessand the audit entry'sdecision, soeffect: "Allow"producedaccess: "Allow", a verdict string no consumer parses, and the same literal comparison silently dropped anyapproval: requiredthe rule carried, re-entering #109's defect class through a typo in a different field.§6.1.5 now states the
effectvalue closure and the entry-point requirement, withdefault_effectstated on the same terms rather than left correct-by-convention. The rejection names the offending value, and the rule index only where the entry point has one — a rule under construction has no position yet, which the apcore-python and apcore-typescript implementations reported independently. Closing the doors is the mechanism: once every entry point rejects, the decision read becomes total over the closed set rather than a fallback with a default arm, and no evaluation-time branch for an unrecognised value is reachable through any API the specification defines. An implementation MUST NOT resolve an unrecognisedeffectto a decision, nor pass it through as one. This IS an SDK change in all three. -
requires_approval: falsewas documented as "no consent needed", which v1.28.0 made untrue (spec v1.29.0, #110). The JSON Schemas said "true = AI must ask for user consent before calling", §3's table said "Whether requires human approval before execution", and §3's AI reading guide listed only thetruecase. Read literally,falsetold a client no consent was needed. Since v1.28.0 the annotation is one source among several and §6.9 rows 3–5 compose them by union, so an ACL rule carryingapproval: required, anExecutionPolicyoverride, orgate_destructivecan require approval for a particular call on a module whose annotation saysfalse.Nothing was unsafe: the approval gate still fails closed and asks the human at execution time. What was lost is the client's ability to say in advance that a call will need approval — which is the whole reason the annotation is read before calling. Demonstrated against apcore-python: a module declaring
requires_approval=Falsewith the driving §6.1.7 ACL shape reportsvalidate(..., {remote, force: True}).requires_approval == Trueandvalidate(..., {remote}).requires_approval == False, while the static annotation staysFalsein both.The annotation describes the module;
validate()(§7.9.5) describes the call. Both JSON Schemas, §3's annotation table, §3's reading guide anddocs/features/module-interface.mdnow say so.Deliberately NOT specified: a
conditionaltri-state. The module author cannot know the answer — it depends on the ACL the deployment loads and the policy it configures, neither visible when the annotation is written. A third value would have to be computed by the framework from the loaded governance config rather than declared, which isvalidate()with extra steps and a breaking change to a field every consumer reads as a boolean.No behaviour change and no SDK change.
-
An unevaluable approval rule stepped aside and the call was granted without approval (spec v1.29.0, #109). The shape §6.1.7 was written for is a narrow approval rule ahead of a broad allow —
git push --forceneeds a human,git pushdoes not. When the narrow rule's condition could not be evaluated, §6.1.1 resolved it to "does not match, MUST NOT grant" and scanning continued; the broad rule then granted, carrying no requirement of its own. The result wasallowwithapproval_required: falseon exactly the call the operator gated, withmatched_rule_indexnaming a rule that never mentioned approval. Reproduced in all three SDKs.The root cause is a section that outlived its assumptions. §6.1.1 was written in v1.22.0, when a rule carried one axis. "An
allowrule MUST NOT grant" was a complete instruction then: it means the rule steps aside, and stepping aside was harmless because whatever granted next also saidallow. v1.28.0 gave rules a second axis and did not revisit it, so "does not grant" began silently discarding the approval requirement too.It is not confined to the legacy boolean. The trigger is an unevaluable approval rule, and §6.1.1 is the path that misconfiguration, §6.1.2's warn-don't-fail registration ordering and handler failure all take. A misspelled predicate (
has_keysforhas_all_keys) or an unregistered condition key reaches it with a governance projection present, on the ordinary Executor pipeline;default_effect: allowreaches it with no second rule at all.validate_rules()is not a mitigation — it cannot see the projection-absent route, and §6.1.2 makes an unregistered condition key a warning rather than a load failure.§6.1.1 rule 5 makes the requirement pending rather than discarded. It is recorded when the unevaluable
allowrule'scallers/targetsmatch, composed by disjunction with whatever grants later — a subsequentallowrule ordefault_effect: allow— and cleared when the final decision isdeny. A rule whose patterns do not match raises nothing, so a rule written about one caller cannot attach a human to calls it was never written about. A rule whose own pattern field is malformed does raise it: its scope cannot be read, so it cannot be shown not to apply, which is the posture that field already produces underdeny, where it denies every call.Requiring a human rather than denying is deliberate. The condition that could not be evaluated is the one that decides whether this call is the dangerous one. Refusing would turn every ordinary
git pushinto the hard failure §6.1.7 exists to eliminate; "ask" is the answer that is wrong in neither direction.§6.9 rows 1 and 2 are amended — the requirement may originate in a rule that did not match, and
default_effect: allowcarries it, makingapproval_required: truewithmatched_rule_index: nulla legal combination. §6.8.1's fail-closed rule is restated as a property of the decision rather than of the matched rule.Backward compatible for correct configurations: across all 20 cases of
acl_argument_scoped_approval.jsonwith a projection present, no decision changes. Without a projection, two change, bothapproval_required: false→true— and both therefore flip the legacycheck()boolean fromtruetofalse, including for a benigngit pushthat is authorized and needs no human. Every non-Executor caller ofcheck()takes the no-projection path by default, so tooling reading that boolean against an ACL carrying anarguments-conditioned approval rule now sees a refusal where it saw a grant. That is §6.8.1's "wrong in the benign direction" working as designed — the condition that decides whether this call is the dangerous one could not be evaluated — but it is a visible change forcheck()consumers, not only a governance-internal one. This IS an SDK change in all three. -
Fourteen Rust examples imported symbols the SDK does not have, and CI could not see any of them (#105).
conformance/check_doc_examples.py's Rust arm matched only the crate-root brace formuse apcore::{...}. Every nested import —use apcore::errors::X,use apcore::events::{Y}— matched nothing and was never examined. In the current tree that is 56 crate-root imports against 56 nested ones, so roughly half of the apcore imports in Rust examples were unchecked.check_doc_examples.pynow resolves nested paths against a module map built from the SDK'ssrc/tree, reporting both an unknown module and a module that does not export the named symbol. A module that glob-re-exports maps to "contents not enumerable" and its symbols are skipped, so the check produces no false positives in the one direction a checker must never produce them. All fourteen findings were verified against apcore-rust before being fixed; none was a false positive.Four of the fourteen named types that do not exist anywhere:
apcore::errors::PipelineStepError,apcore::middleware::MiddlewareContext,apcore::trace_context::TraceContextError,apcore::observability::UsageSummary. Those examples were not merely mis-imported — they taught API shapes the SDK does not have, and fixing them meant rewriting the example bodies:core-executor.mddemonstratede.is::<PipelineStepError>()anddowncast_ref, i.e.std::error::Errordowncasting.PipelineStepErroris anErrorCodevariant, not a type; the real API isModuleError::is_pipeline_step_error()/unwrap_pipeline_step_error(), with the step name and cause indetails.observability.mdmatchedErr(TraceContextError::InvalidParentId)fromTraceContext::try_inject. There is no such method and no such error type: the checked form isinject_checked, returning aModuleErrorcarryingErrorCode::InvalidParentId.observability.md'sUsageExporterimplementation had both halves of the signature wrong —export(&self, _summary: Vec<UsageSummary>) -> apcore::Result<()>against a realexport(&self, summary: &Value) -> Result<(), ModuleError>.apcore::Resultdoes not exist either.
The remaining ten were wrong module paths for types that do exist:
ContextKey,ErrorFormatterRegistryandTraceContextlive at the crate root rather than undercontext/errors/observability;A2AAuthandA2ASubscriberare inevents::subscribersand not re-exported fromevents;ModuleDescriptorandDependencyInfoare inregistry, notmodule;utils::call_chain,sys_modules::registrationandsys_modules::errorsare not modules at all. -
A string where a list belongs turned an
allowrule into a wildcard (#106, spec §6.1.4.1).callers: "admin.*"written wherecallers: ["admin.*"]was meant is iterated character by character in apcore-python, and the*character matches everything — so the rule granted access to every caller underdefault_effect: deny. Measured, not theorised:"admin.*"and"*"both returnedtruefor an unrelated caller;"api.gateway"returnedfalseonly because none of its characters happen to match, which is luck rather than design. A non-subscriptable scalar raisedTypeErrorout ofcheck()in apcore-python and apcore-typescript, violatingContract: ACL.check's "check MUST NOT raise to indicate a deny"; apcore-rust is immune becauseACLRule.callersisVec<String>.ACL.loadalready rejects a non-listcallers/targets; direct construction andadd_rule()did not, which is the same door a non-mappingconditionscame through. §6.1.4's precheck now covers the rule's structure rather than only itsconditionstree, and a malformedcallers/targetsis unevaluable — anallowrule does not grant, adenyrule takes effect, and neither raises.Consequently the deploy-time validator is renamed
validate_rules, since it now reports structural faults outsideconditions. The narrowervalidate_conditionsname has not shipped in any SDK.
-
An ACL condition that could not be evaluated silently disabled the
denyrule carrying it (spec v1.22.0, #100). Condition evaluation returned a plain boolean, so "a handler answered no" and "no answer was obtainable" reached the rule loop identically and both meant this rule does not match. That is safe in one direction only: anallowrule that cannot evaluate its condition does not grant, but adenyrule that cannot evaluate its condition does not block — evaluation continues to the next rule and then todefault_effect. A single misspelled key (role:forroles:) turned a rule its author believed was blocking into decoration. Reproduced end-to-end witheffect: deny+ a misspelled key +default_effect: allow, wherecheck()returnedtrue.PROTOCOL_SPEC §6.1.1 now names the three situations that make a condition unevaluable — no registered handler, a handler that raised/threw/panicked, an async handler unresolvable on the sync path — and requires the rule to resolve toward refusing access: a
denyrule takes effect, anallowrule still does not grant. §6.3's algorithm is restated over three outcomes instead of two, §6.5 gains the three rows, and §6.3.1 documents theAuditEntryall three SDKs already emit — includinghandler_error, which no section had ever defined despiteconformance/fixtures/acl_handler_error.jsonasserting on it.§6.1.2 handles discovery without breaking bootstrap order.
register_condition()writes to a runtime, process-wide registry andacl.rootdiscovery commonly runs before application code, so loading MUST NOT fail on an unregistered key; it MUST warn, naming the rule index, the key and the rule'seffect, and implementations MUST provide an explicit validator to run once registration is complete — covering direct construction and runtime insertion, not only file loading.This is an SDK change in all three, and it changes a decision.
conformance/fixtures/acl_handler_error.jsonpins the opposite behaviour today, under a case namedthrowing_handler_does_not_flip_default_allow_to_deny_unsafely. The corrected fixture is staged atplanning/acl-unevaluable-conditions/staged-fixtures/and lands after the three drivers, so CI does not go red across every SDK repository for the duration of the rollout.Deliberately out of scope: §6.5's "conditions present but no context provided", which stays a non-match. Calling with no context is a legitimate shape for external entry points, not a misconfiguration, and treating it as a failure would flip the decision for every
@externalcall meeting a conditionaldenyrule. It gains a warning and an explicit note on the consequence instead.Precedent for the rule: §7.9.4(4) has required since v1.9.0 that a typo cannot silently disable an execution policy. ACL rules now carry the same guarantee.
-
schemas/acl-config.schema.jsoncontradicted the condition extension point it was validating against.$defs.RuleConditionswasadditionalProperties: false, which rejects every key registered through the documentedregister_condition()API, and it listedtime_windowas though it were built in — no SDK registers such a handler, anddocs/guides/acl-configuration.mdpresents it as a custom handler example. A reader who copied that guide got a schema-valid file whosedenyrule never fired. The condition set is now open, with the reason recorded in thedescription, andtime_windowvalidates as any other custom key.
-
ACLhad no read-only accessor fordefault_effect(spec v1.23.0, #101). It is the single most consequential value in an ACL — §6.1 carries adangeradmonition about setting it toallow— and no SDK exposed it: apcore-rust keeps a private field whose only public reader isrules(), apcore-typescript declaresprivate _defaultEffectwith no getter, and apcore-python'sacl.pydefines no@propertyat all, so neitherrulesnordefault_effecthas a public reader there. The specification was the origin —features/acl-system.mddefined a Contract forcheck,load,discover,add_rule,remove_ruleandreload, and no read-only surface at all, so there was nothing for an SDK to implement. New §6.8 makes both accessors MUST, requires them to be pure reads reachable through a documented public path, forbidsrulesfrom handing out a mutable reference into the ACL's own list, and requires both to reflect areload(). Without it, tooling that reports or audits the enforced policy had to re-read and re-parse the ACL file to recover a value the loaded object already held — a copy that can drift acrossreload(), and on TypeScript and Rust the only option available at all. Additive and backward-compatible in every language. -
Policy resolution could not see the call's arguments (spec v1.24.0, #102). Governance decided on which module was being called and never on what it was being called with: resolution took a module ID and the module's annotations, and a
PolicyRulecarried a module-ID pattern plus two boolean overrides — the shape §7.9.1(2) mandates, identical in all three SDKs. The data was never missing: the approval gate is Step 5 and the invocation's arguments andContextare in scope at the call site, which passed only the module ID. An operator who needs to gate some calls to a module therefore has to gate all of them, producing audit noise and weakeningrequires_approvalfrom "this needs approval" to "this might" — the distinction §7.9.3 exists to preserve.New §7.9.6 requires resolution to receive the call site while forbidding the built-in pattern rules from consulting it, so a rule set's verdict stays a function of module ID and annotations alone and remains reproducible from the policy document. Two constraints are explicit: those arguments have not been schema-validated, because the gate is Step 5 and input validation is Step 7, so a host-supplied policy must not assume them well-formed; and adding the call site MUST NOT change the verdict any existing policy produces. A declarative argument predicate on
PolicyRuleis deliberately not specified — ACL rules already discriminate on caller, target, identity type, roles and call depth while policy rules discriminate on module ID alone, and adding a predicate to only one would grow a second condition language over the same decision point. -
api-surface-conventions.md§9 — how SDK-owned data types are constructed across a package boundary. Written because every Rust approval and preview example in this repository failed to compile (#103). -
api-surface-conventions.md§9.4 — a machine-readable fragment marker. §9.2 rule 5 requires a code block that is deliberately a fragment to say so, which is not an executable rule without a defined syntax. It is now<!-- apcore-example: fragment -->on the line before the fence: an unmarked block is claimed to compile, a marked one is exempt from compiling and from nothing else. Applied to the five blocks whose fragment status was established by compile-checking; the sweep across the remaining Rust blocks belongs to the harness in #105, because marking them by hand beforehand would be guesswork.§9.4 also documents what
conformance/check_doc_examples.pyactually covers, which is narrower than it reads and uneven across languages: Python checksfrom apcore… import …including submodules, TypeScript checksapcore-jsimports and the declared subpath exports, and Rust checks only the crate-root brace formuse apcore::{ … }plusErrorCode::variant names. A nested import such asuse apcore::events::{EventRetryConfig}matches nothing and is never examined. The tree carries 56 crate-root brace imports and 56 nested ones, so roughly half the apcore imports in Rust examples are invisible to it — which is why theRetryConfig/EventRetryConfigmismatch passed CI, and why it was found by hand rather than by the checker.
-
The async-condition rule contradicted §6.1.1 in three places. §6.1.1 lists "an async handler that could not be resolved on the synchronous
check()path" as one of the three unevaluable situations, but three older passages still specified that case as unsatisfied: §6.1's compound-operator paragraph (as "MUST fail closed" — ambiguous once the two outcomes are distinguished, because on anallowrule not-matching is failing closed while on adenyrule it is failing open),features/acl-system.md's "Sync handler resolution" admonition, andspec/design-context-annotations-acl.md§3.6's pseudocode. Left as written, adenyrule guarded by an async-only handler stayed inert on the sync path — the same failure mode #100 was opened for, reached by a different route. All three now say UNEVALUABLE and point at §6.1.1; the design document, which is historical, gains the discrepancy in its superseded-parts list rather than being rewritten. -
validate_conditions()had no defined answer for a key registered on only one path (§6.1.3, new). SDKs keep two condition-handler registries;async_check()consults the async one and falls back to the sync one, whilecheck()consults only the sync one. A key registered only as an async handler is therefore a working condition underasync_check()and an unevaluable one undercheck(). A validator reporting a single "registered" boolean would give a different answer depending on which registry an SDK happened to consult, and could not tell an unregistered key from one that is merely unusable on the path the application calls. Findings now carrysync_registeredandasync_registeredseparately, and a finding is emitted wheneversync_registeredis false — including whenasync_registeredis true. -
handler_errorwas underspecified where it interacts with short-circuiting and with multiple failures. Two determinism holes: a condition skipped by a legitimate short-circuit was never evaluated, so it is now stated not to be unevaluable and not to sethandler_error; and when several conditions in onecheck()are unevaluable,handler_errormust now list them lexicographically by key, not in evaluation order. Evaluation order is not portable —serde_json's map is ordered while Pythondictand JavaScript objects preserve insertion order — so "the first one encountered" would have written a different key into the audit log for the same rule in different SDKs. -
Contract: ACL.add_ruledid not carry the §6.1.2 warning requirement. §6.1.2 rule 4 makes runtime rule insertion an entry point that must be covered; the contract described only insertion and locking. It now specifies the warning, its content, and that insertion still succeeds. -
spec/rfc-preview-method.md's pre-condition section read as outstanding work. It was marked resolved at the top and then listed three numbered steps to perform before Stage 2 SDK work could proceed — all of which have been done. Rewritten as history, with the actual issue link and the full list of types that ended up carrying#[non_exhaustive]. -
features/event-system.mdusedevent.namein all three language tabs. No SDK has that field: it isevent_typein apcore-python and apcore-rust,eventTypein apcore-typescript. Found by compile-checking the block while fixing #103 — and worth recording that this block is a fragment, which did not stop it from being wrong in every tab, and that nothing in CI would have said so. That gap is now tracked in #105. -
Nine Rust examples that did not compile, across five documents (#103).
#[non_exhaustive]forbids struct-expression construction from outside the defining crate, including..Default::default()— verified from a downstream crate against apcore-rust 0.27.0, which produceserror[E0639]. apcore-rust's own integration tests already use the working form and say why; the knowledge never reached the documentation. Corrected infeatures/approval-system.md(trait declaredrequest: ApprovalRequestwhere the SDK takes&ApprovalRequest; two struct literals),guides/cookbook-approval-flow.md(a comment claimingApprovalResulthas noDefault— it derives one; another claimingCallbackApprovalHandler::newtakes a closure returning a Future — it takesimpl Fn(&ApprovalRequest) -> ApprovalResult; two struct literals),spec/rfc-preview-method.md(aPreviewResult/Changeliteral, and a pre-condition section describing#[non_exhaustive]as not-yet-applied when it has shipped),features/async-tasks.mdandguides/middleware.md(threeRetryConfigliterals), andfeatures/event-system.md, which importedRetryConfigfromapcore::eventswhere the exported name isEventRetryConfig.Also corrected in
guides/middleware.md: a comment statinguse_middlewarereturnsResult<(), ModuleError>, stale since spec v1.21.0 gave it aMiddlewareHandle.The two blocks central to the issue —
approval-system.md'sSlackApprovalHandlerandcookbook-approval-flow.md'sPolicyCheck— were made complete and verified withcargo checkagainst the local apcore-rust, which required importingExecutor, definingregistry/config, removing a duplicatelet mut executor, and stubbing theask_slack/slack::ask_approvalhelpers the Python and TypeScript tabs invoke without defining. The rest remain fragments, now carrying the §9.4 marker; §9.2 rule 5 distinguishes the two cases and requires a fragment to be correct in every line it does show. -
guides/cookbook-approval-flow.mddemonstrated a cross-language divergence by accident (#104). Its Python and TypeScript tabs used an async callback withCallbackApprovalHandlerfor a Slack round-trip, and its Rust tab claimed the Rust constructor accepts one. It does not — apcore-python and apcore-typescript take an async callback, apcore-rust takes a synchronousimpl Fn(&ApprovalRequest) -> ApprovalResult— so an approval decision that performs I/O fits the convenience handler in two SDKs and not in the third. That limits the convenience wrapper, not the SDK: theApprovalHandlercontract is async in all three, so apcore-rust expresses an async decision by implementing it directly. The Rust tab now implementsApprovalHandlerdirectly, with the divergence stated rather than papered over. Whether the convenience handler should be capability-equivalent across SDKs is open in #104.
-
Seven specification passages that no implementation satisfied, or that contradicted another passage. Each was verified against the three SDKs before editing; none required an SDK change.
-
Contract: SubscriberCircuitBreaker.on_failuredescribed an API nobody implements. It declared a requiredsubscriber_idinput — the breaker is per-subscriber and already knows which one it guards, and no SDK accepts the argument — and declaredCircuitStateas the return, where apcore-python (circuit_breaker.py:125), apcore-typescript (circuit-breaker.ts:107) and apcore-rust all return the optional lifecycleApCoreEvent. Both corrected to the shape all three ship. -
apcore.event.delivery_failed's payload was declared two ways. PROTOCOL_SPEC §7's event table listedevent_type,reason,subscriber_id;features/event-system.md§ Dead-Letter Queue listssubscriber_type,subscriber_id,original_event,error,attempt_count,timestamp. The two overlap on one key. All three SDKs implement the feature-page shape, so the spec table — what a consumer building from the normative document would follow — was the wrong one. It now defers to the feature page. -
bindings.fileswas a MUST nothing could satisfy. No SDK implements it,BindingsConfigisadditionalProperties: falseover{dir, pattern}so the key is schema-invalid, andconfig_key_governance.jsonallows only those two. Withdrawn, with the reason recorded, rather than left standing as an unmet requirement. -
The reserved error-code prefix list named four (
MODULE_/SCHEMA_/ACL_/GENERAL_) inside a normative block. The canonical set is fourteen, stated infeatures/error-system.mdand matchingFRAMEWORK_ERROR_CODE_PREFIXESin all three SDKs. A module author reading only the specification would pickCONFIG_*orBINDING_*and be rejected by every implementation. -
§12.8.5 cited "
call()Step 6" for the schema validationvalidate()reuses. Step 6 has been the Middleware Before Chain since v0.18; input validation is Step 7, which §12.8's own opening already said. -
registry-system.md's reserved-namespace table listed five of the eight prefixes §2.5 reserves, omittingplugin.*,schema.*andacl.*, and describedapcore.*asregister_internal()-only where §2.5 records it as reserved with no current use. All eight are now listed, with a note on whyephemeral.*is enforced by prefix rather than as a reserved word. -
core-executor.mddescribedGovernanceStateas "eight booleans" directly above a table listing nine. PROTOCOL_SPEC §6.6.5 has it right ("eight observations plus one derived flag"); v1.16.0 added the ninth field without updating the prose.
-
-
dependenciesis now a parsed field on the module descriptor in all three SDKs (spec v1.18.0, #90 follow-up). PROTOCOL_SPEC §12.2 has required since v1.10.0 that adependenciesentry inmetadatareach the registered module's descriptor "so thatget_definition(module_id).dependenciesreturns what the caller declared", and the v1.10.0 row closed with "No SDK behaviour change: all three already satisfy the requirement."That was true of the data surviving and false of the accessor. Only apcore-rust carried a parsed
Vec<DependencyInfo>. apcore-python surfaced the unparsed list nested underdescriptor.metadata, sohasattr(descriptor, "dependencies")was False. apcore-typescript surfaced it on neithergetDefinition().dependenciesnordescriptor.metadata—mergeModuleMetadataextractsdependenciesas a canonical field, so it never landed in the metadata bag either.That had a shipping consequence:
system.manifest.*in apcore-typescript readdescriptor.metadata['dependencies']and therefore reporteddependencies: []for every module that declared them, while apcore-python reported the real list over the same wire contract. Verified before and after; the two now emit identical payloads.The requirement is restated as a parsed field because the distinction is the point.
metadatais defined by theget_definitionReturns table as arbitrary extension data — thex-layer of the three-layer model — while dependencies are structural data the framework itself consumes for load and reload ordering. Carried in the extension bag, the{module_id, version?, optional?}parse falls on every consumer:sys_modules/control.pyimportedparse_dependenciesinside its reload function to do exactly that.§12.2's rationale is corrected alongside. It read "Reload ordering reads that accessor", which holds only for apcore-rust — apcore-python and apcore-typescript order reloads from
get_module_metadata(). That is the sole reason the missing field never surfaced as a bug, and it left a trap: refactoring either reload path onto the more natural-lookingget_definition()would have silently degraded ordering to its sort's alphabetical seed order. Tests in both SDKs now assert the two accessors agree.features/registry-system.md'sget_definitionReturns table gains thedependenciesrow it never declared — the table listed twelve fields while the register Contract on the same page referenced a thirteenth.displayandenabled, which apcore-rust also carries on its descriptor and the table also omits, are deliberately left open rather than bundled:displayis accepted as a parameter by all three but promoted only by Rust and is named nowhere in the specification, andenabledis not a shared concept — Rust has a registry-level flag mutated byRegistry::enable/disablewhile the other two route enable/disable throughToggleState(system.control.toggle_feature). Unifying that means deciding which enable/disable mechanism is canonical, which this change has no business answering.Governance: maintainer approval per GOVERNANCE.md § Decision Making; no tracking issue was opened.
-
Contract: APCore.removerequired an identity removal apcore-rust could not provide, for a reason it had recorded wrongly (spec v1.21.0). The contract removes by IDENTITY: apcore-python and apcore-typescript take the middleware object back and compare withis/===. apcore-rust exposedremove(&str)plus aremove_middleware(&dyn Middleware)that resolved toremove(middleware.name()), and a doc comment explained the divergence as "trait objects do not support identity comparison".That is false —
Arc::ptr_eqhas ignored vtable metadata since Rust 1.76, below the crate's MSRV — and false in the direction that made the gap look unfixable. The real obstacle is ownership:use_middlewareconsumes theBox, so a caller has no pointer left to compare against. The reason determines the fix, which is why the wrong one mattered.Reachable, not theoretical: duplicate registration only warns and always succeeds, so two instances answering one
name()coexist. Verified against the pre-fix code — a caller holding the second of two"audit"middlewares called remove and lost the first.use_middlewarenow returns aMiddlewareHandle;remove_handle(handle)removes exactly that registration. It mirrorsEventEmitter::subscribe→unsubscribe_handle, which exists for the same reason on the event bus. Two normative statements follow: an SDK that cannot take the middleware object back MUST provide a token issued at registration that removes exactly one registration, and MUST NOT present a name-based removal as satisfying this contract. Backward compatible — the added return value is discarded by every existing?;call site, and both name-based forms remain. -
Contract: Registry.registerdescribed a TypeScript API that does not exist (spec v1.20.0). It declaredasync: falseandvoid (TypeScript);registerthere returnsPromise<void>.on_loadis synchronous in apcore-python and apcore-rust — the Rust trait signature enforces it — while apcore-typescript additionally accepts an asynconLoadand resolves once it has run. Everything else about registration is synchronous in all three: ID validation, the duplicate check and every other error throw synchronously, and a module with noonLoador a synchronous one is visible before the promise resolves.This is not only a reader-facing inaccuracy. A module written with
async def on_loadand registered through apcore-python was published and callable with none of its initialisation having run: the coroutine was created, never awaited, and discarded, leaving only aRuntimeWarningat the next garbage collection, attributed to whatever code happened to be running then. That is exactly the half-initialised module the deferred-publish design exists to prevent, reached through the one path that skipped the check.The contract states the real shape, and gains two normative statements: an SDK whose
registerawaits an async load hook MUST keep the module invisible until it completes, and MUST NOT publish a module whose load hook it cannot run. apcore-python now refuses an awaitableon_loadwithMODULE_LOAD_ERRORand leaves the module unpublished — the same outcome any other failingon_loadgets. Nothing can depend on the previous behaviour, since anasync def on_loadhas never once run.The version-history table also gains its missing 1.19.0 row: the §9.14 recursion change bumped the header and the description but never recorded itself in the table.
-
check_expected_keys_read.pyreported "0 unread" partly by accident. Itsis_read()searched a key literal across EVERY test file in each SDK repo, with no association to the fixture that declared it. Measured on the current tree:stream_aggregation'sbmatched in 124 unrelated files,schema_strict_conversion'stypein 108,propertiesin 57. The visible symptom was the reverse direction — it reported the live allowlist entryredaction_config.json: amountas stale on the strength of one unrelated schema-coercion test in apcore-typescript.Scoping the search to a fixture's own drivers, the way
check_driver_coverage.pyalready resolves that relationship, then exposed the opposite failure: the BEST-written drivers name no key at all.usage_contract's three drivers each end in a loop overexpectedcomparing every entry, and were reported as leaving five keys unasserted. So the checker now also recognises a driver that iterates or deep-equals the wholeexpectedmap, in each language's idiom.Two allowlist entries were removed rather than kept:
sensitive_keys_default.jsonandredaction_config.jsonwere exempted for exactly the reason the checker now detects.approval_gate.json's stays — no SDK exposes the code-to-HTTP mapping it names.Verified by removing the wholesale assertion from all three
usage_contractdrivers at once: the checker reports its five keys, and reports nothing when only one of the three is removed, which is the correct reading of "no driver asserts it". -
The documented config
version:was an SDK release number in nine places.apcore-config.schema.jsondescribes that field as "Configuration version" and offers1.0.0as its example. PROTOCOL_SPEC §9.6 filled it with0.14.0across seven blocks,config-bus.mdwith0.15.0, and apcore-typescript's README with0.26.0— three different values for one field, none of them a configuration version, every one stale the moment the next release shipped. §9.1's own example, on the same field, uses1.0.0.Not cosmetic: a reader copies the block, and the number they copy looks like something to keep in step with their dependency. The same misreading seeded apcore-rust's default table, which carried
version: "0.16.0"as "the frozen baseline spec version" while both peers supplied no default at all.All nine now read
1.0.0.conformance/check_config_version_examples.pyguards it, reading the sanctioned set out of the schema's ownexamplesrather than a second copy of it, and skipping ACL policy blocks —acl-config.schema.jsonowns its ownversionand accepts the two-part form its examples use. Wired intoconformance-integrity.yml. -
The §8.2 reachability rule taught itself with a resolved example.
api-surface-conventions.mdpresentedapcore.registry.registry.MAX_MODULE_ID_LENGTHas a live break "tracked in the apcore-python repo". Verified against apcore-python 0.27.0: the constant is exported from bothapcoreandapcore.registry, and named in both__all__s. Kept as the worked example — it is what the rule looks like when it fires — but marked as fixed rather than outstanding. -
sys-health-*.schema.jsonrejected the output every SDK emits. Thestatusenum was["healthy", "degraded", "unhealthy"]. All three SDKs classify ashealthy/degraded/error/unknownand emitunhealthynowhere; neither does any page underdocs/,conformance/orREADME.md.system-modules.md's own classification table lists the four the SDKs use. Corrected to those four, along withsys-health-summary.schema.json'sunhealthyCOUNT field, which splits intoerrorandunknownfor the same reason.Two adjacent gaps surfaced while validating real output against the corrected file: the summary's
modules[]items declared onlymodule_idandstatusunderadditionalProperties: false, while all three SDKs also emiterror_rateand atop_errorobject — so the schema rejected a conforming payload on two counts, not one. Both are now declared,top_errorwith the{code, message, ai_guidance, count}shape the three SDKs build identically.Verified by validating apcore-python's live
system.health.summary/system.health.moduleoutput and the worked example indocs/features/system-modules.mdagainst the corrected schemas, and by confirming the previous version rejects both. -
§9.14's unknown-key walk is recursive, and was specified as one level (spec v1.19.0).
schemas/apcore-config.schema.jsonand its siblings areadditionalProperties: falseat every level, not only at the section root:observability.tracing,acl.audit,validation.bindingandobs.redactionare each closed in their own right.reject_unknown_framework_keysiterated only a section's direct children, so strict mode was blind exactly where a typo is hardest to spot —observability.tracing.sampling_ratpassed the check because its parenttracingis declared, while the canonical schema rejects it, and the misspelled sampling rate fell back to its default with no error and no log line.The pseudocode was the outlier, not the schema. apcore-typescript already walked the full depth; apcore-python and apcore-rust matched the one-level pseudocode and are corrected. This is the specification catching up to a closedness it already declared, so it is a MINOR bump rather than a new requirement:
strictstill defaults tofalse, and no configuration changes behaviour unless its author opted in.An undeclared subtree is reported once, at the point it stops being declared, rather than once per key beneath it — a misspelled section otherwise produces an error per leaf and buries its own cause.
Both SDKs' key surfaces moved from a
section -> direct child namesmap to the flat dot-path list apcore-typescript already used, since the map shape could not express nested closedness and so could not guard it. Their drift guards moved with it and now compare full paths at every depth. Two follow-on gaps surfaced from that: apcore-rust's load-path coverage guard read the section table and therefore never noticed that$schema— a top-level key an operator can write and the schema accepts — had no load-path coverage at all, and apcore-rust had no equivalent of apcore-python's guard that every section enforced as closed is closed upstream. Both added.Governance: maintainer approval per GOVERNANCE.md § Decision Making; no tracking issue was opened.
-
PROTOCOL_SPEC §9.15.3 declared the
sys_modulesactivation flagsTruewhile citing a schema that saysfalse(spec v1.17.0). TheConfig.register_namespace("sys_modules", ...)block passesschema="schemas/sys-modules.schema.json"and then, four lines below, declaresdefaults={"enabled": True, ..., "events": {"enabled": True, ...}}. That schema declares both keysdefault: false. So does §6.6.3 of the same document, which states the 0 / 6 / 9 activation ladder in terms of exactly those two flags; so doesconformance/fixtures/config_defaults.json; and so do all three SDKs. §9.15.3 was the only dissenting authority, and it dissented from a file it names on the preceding line.Read literally it told implementers to register the six read modules in every project that never asked for them, and — through
events.enabled— the threesystem.control.*write modules, which are the approval-gated control plane. Both activation flags are nowFalse. The per-module sub-flags (health,manifest,usage,control) stayTrue: they select which modules register once activation has happened rather than activating anything themselves, andsys-modules.schema.jsondeclares themtruefor the same reason. Only the two keys the schema disagreed with were touched.No behaviour change: no SDK, schema or fixture moves. It does explain a real cross-language divergence found in the same sync — apcore-rust's
Config::namespace()merges registration defaults on every call in both config modes, so in legacy mode it reportedsys_modules.enabled = truesourced from this block while apcore-python and apcore-typescript reported the schema'sfalse. Rust was faithfully implementing one half of a self-contradictory specification; correcting §9.15.3 removes the contradiction at its origin rather than papering over it in an SDK.Governance: maintainer approval per GOVERNANCE.md § Decision Making; no tracking issue was opened, recorded here rather than pointed at an invented number (the precedent set by the 1.13.0 row).
-
docs/spec/algorithms.mdA20guard_call_chain()contradictedconformance/fixtures/call_chain.jsonon 8 of its 11 cases. The pseudocode assumedcall_chainEXCLUDES the module being called, whiledocs/features/call-chain-guard.mdstates it "already includesmodule_idat the end, as set byContext.child()" and the fixture pins that reading. Under the published algorithm the ordinary chain[a, b, c]callingcraisedCIRCULAR_CALL, a single-element chain raisedCIRCULAR_CALL, and a 32-element chain at the default limit of 32 raisedCALL_DEPTH_EXCEEDED. An implementer followingalgorithms.md— which the index marks MUST — could not pass the conformance suite.Corrected on four axes, each verified against all 11 fixture cases: the depth and frequency comparisons are strict (
>), so a chain sitting exactly on a limit passes; cycle detection scans the prior chain for a last occurrence with entries after it, so[a, a]is a self-call and[a, b, a]is a cycle; the limit floors (max_call_depth < 1,max_module_repeat < 1) are rejected before the chain is inspected at all; and the parameter is namedmax_call_depth, matching the feature doc and all three SDKs rather than the algorithm'smax_depth. The fixture'sINVALID_LIMITlabel is documented as a fixture-level expectation, not a wire code — each SDK raises its own idiomatic invalid-input error there (divergence T-B-005). -
README.md's component-version table was stale in five of its six rows. Protocol specification read 1.12.0 against an actual 1.17.0; the core SDK line read 0.26.0 against 0.27.0; apcore-mcp 0.17.2 against 0.18.1; apcore-a2a 0.4.4 against 0.6.0; apcore-cli 0.10.4 against 0.10.5. Verified against each repository's own build config (pyproject.toml/package.json/Cargo.toml), and all four adapter families carry the same version across their three languages. Only apcore-toolkit (0.10.1) was already correct. Nothing checks this table, so it drifts silently while reading as the authoritative release inventory. -
docs/spec/algorithms.mdA24.1deep_merge_chunksdiscarded data at the depth cap, contradicting its own Properties note. Step 1 of the helper wasIf depth >= max_depth → Return, a bare return that drops the override sub-object — while the Properties block twelve lines below states "the truncation point loses no data",docs/features/streaming.mdstates "the right value replaces the left at that level", andconformance/fixtures/stream_aggregation.jsonpins right-value-wins asdeep_merge_depth_cap_right_wins. The pseudocode encoded the exact bug that fixture tracks as divergence T-B-002, so an implementer fixing apcore-python or apcore-typescript againstalgorithms.mdwould have reintroduced it. The cap now assigns the override wholesale and stops recursing. The inline case count for that fixture was also stale (9 against an actual 10).
-
conformance/fixtures/usage_contract.json(11 cases) andgovernance_state.json(12 cases), driven by all three SDKs (#96, #97). The two sections that were marked PENDING now cite landed fixtures, anddocs/features/core-executor.mddrops its not-implemented banner:governance_state()ships in apcore-python, apcore-typescript and apcore-rust.usage_contract.jsonpins the two value semantics no JSON Schema can assert — a full-historycall_countand an off-by-onep99_latency_msare both well-typed numbers in the right field.governance_state.jsonincludes the lookalike case (a custom step namedacl_check) and the three cases that discriminate the corrected v1.16.0 derived flag from the unsound one published in v1.15.0.Both were verified against a mutated fixture before landing: flipping the discriminating governance case to the v1.15.0 answer, and the p99 worked example to 100, reds exactly those two cases in all three SDKs. A case that cannot go red is not coverage.
Two adjustments the drivers forced, recorded in each fixture's
driver_contract. Backdated records are expressed asat_offset("-2h") rather than absolute instants, because the SDKs window against the real clock and a fixed date would put every record outside every window — the period cases would pass for the wrong reason. And the unattributed-caller case must be recorded through the SDK's own usage-recording path, not by handing a null toUsageCollector.record(): apcore-python and apcore-typescript substitute"unknown"inUsageMiddlewarewhile apcore-rust does it in the caller breakdown, so driving the collector directly tests a different API in each SDK.
-
unprotected_control_surfaceas published in spec v1.15.0 was unsound; corrected in v1.16.0 (#97). The formula treated a wired approval gate plus either a handler orExecutionPolicy(strict=true)as sufficient to conclude that a gate stands in front ofsystem.control.*. It is not, because the two gates are not symmetric:acl_checkevaluates every call, whileapproval_gateresolves per module and returns before consulting the handler when the module does not need approval — all three SDKs short-circuit there (apcore-typescriptbuiltin-steps.ts:401, apcore-pythonbuiltin_steps.py:453, apcore-rustbuiltin_steps.rs:623). Since §6.7 makesrequires_approvalon control modules a SHOULD, an ordinary conformant deployment can registersystem.control.*modules that the gate never engages on, and the published flag reported that as gated. A falsefalse— the one direction §6.6.5.2 declares the flag must never fail in, written into the formula by the same change that declared it forbidden. Adds a ninth fieldall_control_modules_require_approvalas a required conjunct, new §6.6.5.1.1 explaining the asymmetry, and four fixture cases of which three discriminate the corrected formula from the published one. No SDK had implementedgovernance_state(), so nothing shipped the wrong behaviour. -
Two
Pinned by conformance/fixtures/...citations named files that do not exist (#96, #97). §6.6.5.4 citedgovernance_state.jsonand §6.7.1.6 citedusage_contract.json; both are held inplanning/until the SDKs implement and drive them, which the same change documented. A citation that reads as coverage while nothing is asserted is the exact defect this repository's fixture guards exist to prevent. Both now say PENDING, link the planning draft, and state that the sections are specified-but-unverified.docs/features/core-executor.mdlikewise gains a warning thatgovernance_state()ships in no SDK today — it was showing call examples for an API that does not exist.A pre-existing instance of the same defect is fixed alongside: §"Display Resolution" cited
conformance/fixtures/display_resolve.jsonas "already created". It was never created, and no other fixture covers display resolution. -
The §2.1 reserved-word mitigation overstated what the SDKs enforce (#98). The replacement text said §2.6's per-segment rule and "the equivalent check in each SDK's public
register()". They are not equivalent: all three check the first segment only (registry.py:286,registry.ts:204,registry.rs:388, each commented "reserved word first-segment check"), sofoo.system.baris rejected by §2.6 as written and accepted by every implementation. The mitigation now describes the implemented behaviour — sufficient for T8, because only the first segment can impersonatesystem.*— and records the spec/implementation divergence explicitly instead of papering over it. The §2.6 divergence itself is a real, pre-existing spec bug and is tracked separately. -
The
sys.guard's allowlist widened itself (#98). The pattern matched one segment aftersys., so an entry keyed onsys.controlexempted everysys.control.*there is — a newly-introducedsys.control.shutdownwas silently allowed, which is the one thing the guard exists to catch. The pattern now matches the full dotted ID, allowlist keys are full IDs, and host-language / config-path exclusions compare the first two segments. Theconformance-integritypath filter also now coversexamples/**,CONTRIBUTING.mdandllms.txt, which the guard scans and the filter did not. -
Three count contradictions in the #96 material. §6.7.1 said the implementations "diverged in four ways" where §14 and this file say five (five is right — the issue's four plus the
periodgrammar).planning/usage-contract-parity/overview.mdsaid16/16for a set that is fifteen plus two. And §6.7's preamble still said "the SDK source is the schema source of truth", contradicting §6.7.1 declaring the two new files canonical; it now states that a shipped canonical schema wins and the SDK source is the description of record only where none exists.
-
PROTOCOL_SPEC §6.7.1 "Usage Module Output Contract" and the two
system.usage.*schemas (spec v1.14.0, #96). §6.7 named the two usage modules, required "equivalent input/output schemas", and deferred the field contract to each SDK's source. Three implementations diverged in five ways without any of them becoming non-conformant, because nothing said what the fields mean. Now normative:periodis a filter, not an echo. Grammar^[1-9][0-9]*[hd]$, declared as apatternininput_schemaso a malformed value fails at input validation withSCHEMA_VALIDATION_ERRORin every SDK — apcore-python's parser accepted"0h","-5d"and"+3h", apcore-typescript's rejected all three, apcore-rust parsed no period at all. Every statistic in both outputs MUST be computed over[now − period, now]; apcore-rust echoedperiodback while every number behind it covered the full retained history, which is silent by construction — the response names the window it did not apply.hourly_distribution[].hourisYYYY-MM-DDTHH, the keyUsageCollectoralready produces in all three SDKs, emitted verbatim. apcore-rust reformatted it to%Y-%m-%dT%H:00:00Zbehind a constant documented as "matchingUsageCollectorbucket hours" whilebucket_keyproduced%Y-%m-%dT%H— internally inconsistent, not merely different. This repository's own example indocs/features/system-modules.mdshowed the reformatted spelling, so the divergent implementation was the one following the docs; the example is corrected here. Exactly 24 entries, ascending, zero-filled; the 24-entry span is fixed andperiodfilters the counts inside the buckets, not the array length.p99_latency_msis nearest-rank —sorted[min(ceil(0.99·N), N) − 1], no interpolation,0on an empty sample set. apcore-python computed that index and then discarded it, returningsorted[rank], one element higher; for 100 samples it answered 100 where apcore-typescript and apcore-rust answered 99. A worked example is stated normatively because no schema can assert a value disagreement inside anumberfield.trendthresholds (> 1.2rising,< 0.8declining, zero-cases decided first) and unattributed calls as the literalcaller_id"unknown"— nevernull, never omitted, never the ACL token@external.output_schema()MUST declarepropertiesandrequired. apcore-rust returned a bare{"type": "object"}for both modules, which satisfies "equivalent output schemas" only in the sense that any two such declarations are equivalent to each other.
schemas/sys-usage-summary.schema.jsonandschemas/sys-usage-module.schema.jsonare the canonical shape,$idonhttps://apcore.dev/to match the existing fifteen. Both carryadditionalProperties: false, and thehourpattern deliberately rejects apcore-rust's current output — that is the assertion that fails until the SDK work lands, and it must not be relaxed to accommodate:00:00Z. -
PROTOCOL_SPEC §6.6.5 "Governance State Query" (spec v1.15.0, #97). A read-only
governance_state()on the Executor returning seven observations plus one derived flag, with normative field names across the three SDKs.acl != nullis not the answer to "what is gating this registry": the ACL and approval gates are pipeline steps, and three of the four strategies this specification defines (internal,testing,minimal) removeacl_check— so an adapter readingacl.is_some()reports "protected" in precisely the configurationset_acl()is already warning about.builtin_acl_gate_wired/builtin_approval_gate_wiredMUST be determined by step type or capability, never by step name, becauseStrategyInfocarries names only and a custom step namedacl_checkwould otherwise report a gate that is not there.unprotected_control_surfaceis defined exactly and is explicitly not a security verdict — it reports the absence of a recognised gate, never the presence of protection; anis_secure-shaped field is forbidden. Purely additive: no default changes, no behaviour changes, and apcore-rust's existing publicacl/approval_handler/policyfields are untouched. -
conformance/check_module_namespace.py— a guard on thesys.module-ID namespace (#98). The control plane issystem.*;sysis not reserved. Host-language spellings (sys.path,sys.exit) and thesys.modules.*Config Bus key path are excluded by construction; anything else needs an allowlist entry with a reason, reported STALE once it stops matching. Wired into theconformance-integrityworkflow.
- PROTOCOL_SPEC §6.6.3 rewritten — Layers 2 and 3 are inactive by absence (spec v1.15.0, #97). Layer 1 registers 0 / 6 / 9 modules across two config flags, not one:
sys_modules.enabledalone gives the six read modules, and the threesystem.control.*write modules requiresys_modules.events.enabledas well, because their audit events need theEventEmitter. A registry with six read-only modules and no ACL is an information-disclosure question, not a control-plane one, and conflating the two produces a warning that fires on a configuration with no write surface. New §6.6.3.1 states that a missingacl/path attaches nothing and MUST NOT synthesize an empty default-deny ACL (which would deny every inter-module call in every project without anacl/directory), and that a missingApprovalHandlerwarns and continues unlessExecutionPolicy(strict)is set. New §6.6.3.2 states that a strategy without the step disables the layer even when the object is configured, with the preset table showing which three do it.
-
The system modules are
system.*, notsys.*— six occurrences, and one of them described the reserved-word mechanism backwards (#98).sysis not a reserved word: PROTOCOL_SPEC §2.5 reserves eight (system,internal,core,apcore,plugin,schema,acl,ephemeral) and the three SDKs reserve the first seven, sosys.control.reload_moduleis an ordinary ID anyone can register, naming a module that does not exist.docs/features/index.mdanddocs/features/error-system.md— wrong namespace in a navigation title and an error-table description. The error codeSYS_MODULE_REGISTRATION_FAILEDis deliberately unchanged; renaming a stable error code is a breaking change.docs/features/core-executor.md— stated that reserved prefixes are permitted "so thatsys.*invocation is legal", which is backwards in both halves. Rewritten to say what the bypass actually does:systemis the reserved first segment, so the bypass is what makessystem.*invocation legal, and every other validation still applies.docs/spec/security-considerations.md— three further occurrences the issue did not count, and the worst of them. The §2.1 mitigation claimed Algorithm A01 rejects IDs beginning withsys.,system.orapcore., "verified by fixturenormalize_id". A01 (directory_to_canonical_id) performs no reserved-word check at all — that is §2.6detect_id_conflictsstep 2 —sysis not reserved, andnormalize_idcovers Algorithm A02 with no reserved-word case in its 16. Three wrong claims in one security mitigation, now corrected, with the absence of fixture coverage recorded rather than papered over. The §3.2 audit checklist told operators to check for ACL rules targetingsys.control.*— a pattern that matches no module and enforces nothing, which is the same silent failure mode as apcore-mcp#14, reproduced inside the security document itself.
-
docs/spec/conformance.md§8.1 fixture inventory was three ways out of date, and CI was red on it.preflight_disclosure(4 cases) was absent entirely,schema_keyword_paritywas recorded as 119 against an actual 122, and the Total read 664 / 60 fixtures against an actual 671 / 61. Theconformance-integrityworkflow verifies all three, somainhad been failing since 019eaa4 and every PR touchingdocs/,schemas/orconformance/inherited the failure. -
The
apcore#96governance citation on spec v1.13.0 pointed at an unrelated issue. The preflight-disclosure change cited a reserved issue number that was never opened; #96 has since been assigned to thesystem.usage.*schema work. The citation is withdrawn in both §14 and the 0.27.0 entry rather than repointed — recording that there is no tracking issue is better than inventing a link to one.
First non-draft release of the specification. Every prior version in the §14 version history is recorded as
X.Y.Z-draft; 1.9.0 is the first finalised one. Governance gate: apcore#79 (linked issue + maintainer approval per GOVERNANCE.md § Decision Making).This section contains BREAKING specification changes. No implementation had provided the superseded v1.8.x behaviour for any of them, verified per item against all three SDKs, so no deprecation cycle is owed and no dual-accept window applies.
Release note: this section contains BREAKING specification changes.
-
conformance/fixtures/preflight_disclosure.json(4 cases) and §12.8.5.1 — a failedaclcheck withholds module-level introspection (spec v1.13.0; no tracking issue — the#96cited here originally was a reserved number now held by an unrelated issue).Executor.validate()looked the module up at Step 3 and ranpreflight()andpreview()at Check 7 on the strength of that lookup alone, so a caller the ACL had just denied still made module-authored code run and still received what it returned. For a command-wrapping module that is the resolved binary and its argv; for a writer it is the target of the side effect. All three SDKs did it, each guarding only on "module lookup succeeded" — andapcore-mcp-rusthad already grown a string-matched disclosure filter over the top (async_task_bridge.rs), which is the evidence the gap was reachable in a shipped product rather than theoretical.validate()now MUST NOT invoke either hook, emit amodule_preflight/module_previewcheck, or populatepredicted_changeswhenaclfailed. The failedaclcheck itself is still reported and no other check is suppressed: the rule is about authorization, not validity, so a malformed input from a permitted caller still gets the module's own account of what would happen — which is exactly what the caller needs in order to fix the call. Fixed in apcore-python, apcore-typescript and apcore-rust.The fixture's control case matters as much as its denial cases: without one, an implementation that never introspects at all passes the denial cases for the wrong reason. Its
driver_contractalso requires observing hook invocation from inside the hook bodies — an implementation that calls the hooks and discards their results still ran module code for a denied caller. -
Three
format-in-a-union cases inschema_keyword_parity.json. The annotation rule (type-mapping §11.1) was pinned only whereformatsat at the top of a property. The sharp case is a value no sibling branch can rescue —{anyOf: [{string, format: email}, {string, maxLength: 3}]}against a 12-character non-email — where an implementation assertingformaton a branch has no matching branch left and rejects. A third case pins thatformatdoes not discriminate aoneOf: twotype: stringbranches both match, so exclusivity fails and the value is rejected, where treatingformatas an assertion would leave exactly one branch matching and wrongly accept. No SDK behaviour change — all three already passed on the first run, so this was a coverage gap, not a defect. -
type-mapping §17 "Validation Keyword Conformance". A normative MUST/MAY table for every Draft 2020-12 validation and applicator keyword, with four general rules: R1 no silent drop (a keyword that cannot be enforced fails at load with
SCHEMA_PARSE_ERROR), R2 keyword inertness on instances of other types, R3 adjacency withtype(both are independent assertions), R4 conformance asserted by fixture. Replaces the "Complex combinators have partial support" note, which was too vague to judge an implementation against. §3.1 / §3.2 / §10.3 extended to the full §6.4 / §6.5 / §10.3 keyword sets. -
conformance/fixtures/schema_keyword_parity.json(119 cases). Keyword handling parity at the validation boundary — the path a module call actually takes. Itsdriver_contractstates this explicitly, because the previous generation of fixtures drove only the raw-JSON-Schema validator, and that is precisely why a whole class of converter defects stayed invisible for several releases. -
conformance/fixtures/schema_strict_conversion.json(16 cases). The exact A23 output all three SDKs must emit. -
conformance/fixtures/openai_strict_compat.json(30 cases). The OpenAI structured-outputs keyword set backingBindingStrictSchemaIncompatibleError, recording its source URL and verification date so it can be re-checked as the provider evolves. -
driver_contractonschema_hardening_formats.json. States which halves of that fixture are normative: thevalid: truehalf holds on all three SDKs at the module boundary; thewarn_loggedhalf does not pin equivalent behaviour, andformat_mappingsis aspirational metadata no SDK implements. -
A23
requiredis emitted sorted (algorithms A23 step 2f, §4.16). The algorithm said only "node.required← all_property_names" and both worked examples showed insertion order, whileschema_strict_conversion.jsonpins a sorted list and all three SDKs already sort. The sort is now normative, specified as Unicode code point order (JavaScript's defaultArray.prototype.sort()compares UTF-16 code units and orders supplementary-plane names differently), and both worked examples corrected. -
§12.2
Interface: Registrystatesregister, andmetadata.dependenciesis now normative (#90). The normative component interface declareddiscover,get,listanddescribe— notregister, the most-used entry point on the component and the one every SDK exposes with a four-argument signature.protocol-spec.md§12.2 now statesregister(module_id, module, version?, metadata?), and requires that when an implementation acceptsmetadata, adependenciesentry — a list of{module_id, version?, optional?}— MUST reach the registered module's descriptor, soget_definition(module_id).dependenciesreturns what the caller declared.That requirement existed nowhere, which is why all three SDKs lost it independently and all three fixed it independently (apcore-python
ad2998d, apcore-typescript#35, apcore-rust71295e1). The loss is quiet by construction: discovery-time dependency sorting reads its own parse and keeps working, soresolve_dependencieslooks healthy while the post-registration accessor returns nothing and a dependency-ordered reload degrades to its sort's seed order — usually alphabetical, therefore plausible, therefore not reported. Convergent behaviour with nothing holding it in place is the arrangement that produced the divergence in the first place.versionis stated as an OPTIONAL parameter only, not as resolution. All three SDKs acceptversionandmetadata; only apcore-python resolves by version. §5.4 continues to govern multi-version coexistence as optional, andget(module_id)keeps its single-argument normative form. Making resolution a MUST would put a requirement into the specification that two of three implementations do not provide — the exact shape 1.9.0 spent a release removing. The ordered side effects, the in-flight reservation and the visibility rule stay infeatures/registry-system.md§ Contract: Registry.register; §12.2 carries the signature and the data-survival requirement. No SDK behaviour change — all three already satisfy it. No anchor ID added, removed or renamed. -
The case-pinning backlog is zero (#92). All 651 measurable fixture cases are now run by at least one driver — mutating any one of them reddens a suite. The starting point was 36 measured against apcore-python alone, and the fixes landed in all three SDKs (apcore-python
2f4daad, apcore-typescript5b3b875, apcore-rust289c2f8). No SDK defect surfaced in any of them: every newly-real assertion passed against the shipped implementation on its first run, so the contracts these fixtures declare were being met all along and simply were not checked. The last one standing wasbinding_errors::pipeline_handler_not_supported_rust— a case that exists to pin a Rust-only behaviour, whose driver hardcoded its message fragments as an OR of two literals and asserted no code at all. -
.github/workflows/case-pinning.yml— a scheduled sweep that fails when a NEW case goes unpinned.check_driver_coverage.pyanswers does each SDK load this fixture,check_expected_keys_read.pyanswers does any driver read thisexpectedkey; neither answers does any driver run this case, and a case every driver skips leaves both green. The new job mutates each case's expectation and diffs the result againstconformance/case_pinning_baseline.json, so the backlog can shrink but cannot grow. Daily rather than per-PR: the sweep runs the real drivers for 651 cases across three SDKs, roughly 20 minutes.
-
BREAKING: a
before_stepfailure terminates the step and is NOT recoverable (middleware-system).before_stepraising and the step body raising previously shared one recovery path, so a value returned fromon_step_errorafter abefore_stepfailure let the pipeline advance past a step whose body never ran. The built-in strategy placesacl_checkandapproval_gatein that sequence, making this a silent authorization bypass reachable from an extension point that carries no authority. The two paths are now separate: on thebefore_steppath the recovery value MUST be discarded,after_stepMUST NOT fire, and the step'signore_errorsMUST NOT apply —MiddlewareChainErrorpropagates regardless. apcore-rust already behaved this way and the spec adopted its behaviour; apcore-python and apcore-typescript are changed to match. Pinned bypipeline_step_middleware.json→before_step_failure_recovery_is_discarded, whose driver contract requires asserting that the following step did not execute rather than merely that an error was raised — a raise-only assertion is satisfied by an implementation that honours the recovery and happens to fail later, while the bypass is live.The module-level precedent does not transfer, despite the shared vocabulary: a module-level recovery value terminates the call and is the return value, whereas a step-level recovery value resumes a pipeline. Same name, different operation.
-
after_stepMUST fire after a recovered step body (middleware-system). A recovered step produced an output and the pipeline continued, so the onion has to close — a middleware that acquired something inbefore_stepwas not getting itsafter_step, which leaks. apcore-python and apcore-typescript already did this; apcore-rust skipped it (pipeline.rsrecovery branch jumped past the hook block). Pinned byafter_step_fires_after_a_recovered_step, deliberately the opposite case to the one above. -
Async-callback detection is an SDK-local concern (middleware-system). The rule prescribed
inspect.iscoroutinefunctionfor Python, which missesfunctools.partialand decorator wrappers; apcore-python gates oninspect.isawaitable()applied to the returned value (Issue #42) and is correct to. Implementations MUST NOT be judged on the mechanism. -
BREAKING:
formatis an annotation, not an assertion (type-mapping §11). §11.1 saidformat"provides semantic validation" and implementations SHOULD validate it, which reads as "a non-conforming value fails". Under the default format-annotation vocabulary of JSON Schema 2020-12 §7.2.1 it does not: a value that does not satisfy its declaredformatMUST NOT fail validation, and a format the implementation does not recognise MUST pass silently. §11 now states this, and points atpattern/enumfor authors who want a binding constraint. Also records that where the SHOULD-level warning is emitted is not uniform — apcore-rust computes format warnings inSchemaValidator::validate_detailed_raw, but its module-invocation path never goes throughSchemaValidatorat all (the executor'svalidate_against_schemabuilds its own validator), so a Rust module call emits no format warning. -
BREAKING: no type coercion at the module boundary (type-mapping §17.3, rule R5). The boundary MUST NOT coerce, under any host configuration. A
coerce_typeslibrary knob MAY exist but MUST NOT reach that path and MUST NOT be readable from configuration. "No coercion" is about instance types, not renderings —4.0still satisfiesintegerper §6.1.1. Removes the last references toschema.validation.coerce_types, a key no SDK ever read. -
BREAKING: self-reference and circular reference are different things (§4.15). The blanket "circular reference detection (A → B → A) MUST throw
SCHEMA_CIRCULAR_REF" made every recursive schema unregisterable, contradicting the Recursive Schema Support requirement stated in the same specification. A$refre-entered after descending through a schema body (properties/items/ a combinator) is a recursive data structure and MUST be preserved as a lazy reference; only a$ref→$refchain reaching no schema body MUST raise. RefResolver MUST NOT inline a self-reference, and the conversion layer MUST bind it at validation time rather than widening it to accept-anything. -
BREAKING:
oneOfexclusivity is location-independent (schema-system). The exhaustive-evaluation rule applies insideproperties,items,$defsand nested combinators, not only at the document root; a root-level combinator MUST be enforced on the generated native model so every call path observes it. -
BREAKING: A23 object detection and nullable spelling (algorithms, §4.16). Object detection is now "carries
propertiesand (typeabsent ortypedeclaresobject)", resolving the contradiction between §4.16 ("All nested object types MUST setadditionalProperties: false") and the A23 pseudo-code (node.type == "object"). The recursion list gainsprefixItems, plusdefinitions/$defswhich all three SDKs already walked but the specification never listed. An optional property with notype— including an author-writtenoneOf/anyOf— is wrapped as{anyOf: [<original>, {type: "null"}]}, never appended to: one nullable spelling, andanyOfbecause OpenAI structured outputs accepts only that. -
schemaconfiguration namespace corrected (§4.9). The example declaredschema.paths.yaml_schemasandschema.validation.*; the real namespace isroot/strategy/max_ref_depth, andschemas/defaults.schema.jsondeclares itadditionalProperties: false— so the documented example was a configuration error. Strictness and coercion are properties of the contract, not of the host. -
pipeline.configurenow has a declared field set, and it is four fields wide (#89).schemas/apcore-config.schema.jsongains$defs/ConfigurableStepFields—match_modules,ignore_errors,pure,timeout_ms,additionalProperties: false— andconfigure's value subschema points at it. It wasadditionalProperties: true, which pinned nothing, and three SDKs accepted three different sets: apcore-python took any attribute of the concreteStepobject (hasattr), apcore-typescript a 9-entry map, apcore-rust a 4-name list. An unknown key was a parse-time error in two of them and awarn-and-continue in the third, so an operator's typo silently produced an unconfigured pipeline on one SDK.spec/DECLARATIVE_CONFIG_SPEC.md§4.2 states the set, states that any other key MUST raisePIPELINE_CONFIGURATION_ERROR, and states that snake_case is the wire spelling — an SDK MAY accept an idiomatic alias at its own API boundary but a configuration file carries the canonical spelling only. -
requires/providesare not configurable, and the specification's own example said otherwise (#89).features/middleware-system.md§ Configuration safety shipped anapcore.yamlsetting them underconfigure:, annotated "matching schemas/apcore-config.schema.json". Run through the shipped apcore-python loader, that exact example moved the built-ininput_validationstep fromrequires=('module',)torequires=('context',)— deleting the upstream dependencymodule_lookupsatisfies, after which strategy construction validates cleanly and thePipelineDependencyErrorthat the same section states as a MUST can never fire for that step. The documented way to exercise the dependency contract was the way to disable it. A step's capability contract belongs to its implementation; the example is replaced with one showing the contract declared on the step class in all three languages, and aconfigure:block using only the four configurable fields. A configuration file carryingrequires/providesis now a startup error, with a migration note in the page. -
DECLARATIVE_CONFIG_SPEC.mdis at spec version 1.1, and no longer calls itself unimplemented. The §4.2 rule above is the normative change. The header also said "Draft for review — not yet implemented" while all three SDKs had shipped thepipeline:section since 0.19.0 — a status line four months stale on a document the schema and three implementations already follow. -
features/registry-system.md§ Multi-version registration described two SDKs wrongly. It said apcore-typescript'sregister(moduleId, module)"always replaces any prior registration" — it takes the full four-argument signature and rejects a duplicate id through A03 conflict detection withDUPLICATE_MODULE_ID— and called apcore-rust's three-argument descriptor form "a Rust-only signature divergence tracked for cross-language alignment", whenregister_versioned(name, module, version, metadata)is the spec-shaped four-argument form and has been there all along. Rewritten around the distinction that actually separates the three: all three acceptversionandmetadata, only apcore-python resolves by version. The page also now records thatmetadata.dependenciesreaching the module descriptor is converged behaviour with nothing normative behind it — §12.2'sInterface: Registrydoes not declareregisterat all — filed as #90. -
The library-level coercion knob's behaviour is normative when the knob exists (#95, spec v1.12.0). Offering the switch stays a MAY; an SDK that offers one MUST coerce exactly
string→integer,string→number, andstring→booleanlimited to"true"/"false"case-sensitive, and MUST NOT coerce anything else.Until now
type-mapping.mdconstrained only where the knob may be used — not on the module-invocation path, not readable from configuration, default off — and said nothing about what it does. Measured through the conformance driver's own path: apcore-rust and apcore-typescript ship a twelve-spelling case-insensitive dialect ("true" | "yes" | "on" | "y" | "t" | "1"and its negatives,validator.rs:443, ported verbatim into TypeScript during #93), while apcore-python coerces no string to a boolean at all. Both were conforming."0"→falseis the sharpest edge: R5 makes the number0a MUST-reject forboolean, so an SDK accepting the string"0"put two of its own paths on opposite sides of one value.The boolean row is capped at JSON's own two literals deliberately.
"true"and"false"are the same value written as text;"yes","on","y","t","1","0"are shell and INI conventions that belong to whatever parsesargv— each one somebody's default rather than anybody's standard.conformance/fixtures/schema_validation.jsongains six cases and adriver_contract. It had pinned the coercing mode cross-SDK in exactly one case,wrong_type_string_for_integer, oninteger— the one axis where all three agreed — which is why the divergence never fired. Four of the six new cases assert a spelling that MUST NOT coerce, because a fixture carrying only"true"is satisfied by an implementation that coerces any non-empty string, which is close to what two SDKs actually shipped. -
Per-SDK case pinning is at zero on all four scopes (#93). 651 cases ×
any+ each SDK — every one now reddens a driver when its declared expectation is mutated. The backlog was 84 gaps over 75 cases: apcore-rust 32, apcore-typescript 26, apcore-python 26, nearly disjoint, with none missed by all three, which is why theanyscope had read clean. Fixed in apcore-python4557fa7, apcore-typescript8472096, apcore-rust321e6f3.Three real SDK defects surfaced, each at the moment a fixture's declared value first reached an assertion. apcore-typescript's
SchemaValidator(true)did not coerce at all — it calledValue.Decode, which applies TypeBox transforms and never converts types, while its own doc comment blocked on "picking a conversion semantics all three can agree on" that the other two had agreed on and shipped. apcore-rust emitted serde'smissing field \bindings`where §7.2 fixes the message and the other two comply; its driver had readexpected_messageand discarded it with a comment that the Rust text "may differ". And apcore-rust'sErrorCode::BindingInvalidTarget` was declared, categorised, and raised by nothing — §2.2 requires target-syntax validation at parse time and none was performed, so a target with no colon loaded silently and failed later as an unrelated handler-map miss.config_defaultswas the headline risk and did not materialise, for a reason worth recording: apcore-rust's driver carried a six-entry allowlist skipping 12 of 18 defaults, justified as "no default in the Rust SDK" — stale sinceCONFIG_DEFAULTSlanded. With it gone all 18 agree.config_key_governance::sdk_reproduces_every_canonical_defaultis the same property one level up and assertedmissing.is_empty()while never readingexpected.missing; both layers empty at once is how it stayed invisible. -
check_case_pinning.pyexportsCONFORMANCE_SPEC_REPOto every driver process. Relying on the ambient environment meant a driver could validate a different checkout than the one being mutated — tests ran,NoTestsRanstayed quiet, and the report was plausible and wrong. This is the fifth bug found in this tool, and the worst: the earlier ones produced a suspicious zero, this one a believable non-zero against the wrong corpus. -
http_statusis documented as descriptive, not a required SDK surface (#94). §7.5 and §8.2 annotate 47 framework error codes with anhttp_status, under a heading reading "Implementations MUST define the following error types". Read as a requirement that would be 47 unimplemented MUSTs — no SDK exposes the mapping and none is expected to, and the HTTP-facing downstream family (apcore-a2a-*) references it nowhere, measured at zero hits across all three. The MUST is about defining the error types; the status is metadata about each, likedescription. §8.2 now says so, andconformance/fixtures/approval_gate.jsondrops theexpected.http_status(403 / 202) that no driver could assert — a fixture may only declare what a driver can check.499is an nginx extension and508is WebDAV: a gateway author's practical shorthand, not a contract three SDKs should reproduce. -
The case-pinning gate now asks the per-SDK question, not just "does SOME driver run this case" (#93). The weak question read as zero while every SDK still had its own blind spots: 84 gaps over 75 cases, only 9 shared by two SDKs and none by all three, which is exactly why the total was clean. Worse, fixing toward it produces residue by construction — it stops at the first SDK that turns a case green, and
call_chain's six positive cases were fixed in apcore-python during #92 while staying broken in apcore-typescript and apcore-rust for that reason alone. The scheduled job now runs four scopes (anyplus each SDK) andconformance/case_pinning_baseline.jsonis keyed by scope, so a regression names the SDK instead of moving a total. Runtime goes from roughly 20 to 80 minutes, which is why it stays scheduled rather than moving to the PR path. -
Three cases recorded in
case_pinning_allowlist.jsonas REQUIRED per-SDK skips (#93). A deliberate skip and a forgotten case are indistinguishable from outside, so each entry carries the evidence from the fixture's own text:binding_errors::pipeline_handler_not_supported_rust(apcore-python and apcore-typescript both resolvehandler:and have no such error to raise),binding_errors::binding_schema_inference_failed_python(apcore-rust uses an opaque handler-map with no runtime inference — DECLARATIVE_CONFIG_SPEC §4.4 "Rust caveat"), andsystem_modules_hardening::rust_register_returns_result(the case pins aResult<SysModulesContext, SysModuleError>return neither peer has). -
Cross-executor rebind is normative, and the deviation clause is withdrawn (#92, spec v1.11.0).
features/core-executor.mdstated it as SHOULD raiseContextBindingError, with "SDKs that choose to accept silently instead MUST document the deviation prominently" as the escape hatch. All three SDKs raise — apcore-pythoncontext.py:152, apcore-typescriptcontext.ts:187, apcore-rustcontext.rs:765— so the deviation was permitted for nobody, and it had a real cost:conformance/fixtures/context_create.jsonexpressed it asexpected_one_of: [raise, silent_accept], and no driver can assert an alternation without deciding its own branch. All three hardcodedraiseand read the alternation only in a comment, so mutating the whole expectation left every suite green.protocol-spec.md§12.2 now states the rule onInterface: Executorwith the wire codeCONTEXT_BINDING_ERROR; the fixture carries a singleexpected. No SDK behaviour change — this pins what all three already do.
-
schema_hardening_formats.jsoncited a section that said nothing aboutformat. The fixture and theconformance/README.mdrow both pointed at PROTOCOL_SPEC §4.15, whose edge-case table covered$refdepth, empty schemas,required: []andenumwithnull— and never mentionedformatat all, so the only citation pinning the behaviour resolved to nothing. §4.15 gains the two rows (a value failing a recognised format is accepted, warning is SHOULD; an unrecognised format is collected as an annotation and passes silently), and both citations now name type-mapping §11.1 as the authority. -
§12.8.5.1 described a
module_preflightbehaviour no SDK provides. It read "ifpreflight()returns an empty list or is not defined, nomodule_preflightcheck is added". The second half is right; the first is not — all three SDKs add apassed: trueentry with no warnings for an empty list, and have since the check existed. The two cases are now stated separately. -
features/middleware-system.md§Pipeline Step Middleware was written without implementation contact. It specifiedbefore_step(step_name, ctx, inputs)and a MUST that a non-null return replaces the step's inputs — a calling convention that exists in no SDK, because aStepisexecute(ctx)and takes noinputsargument. All three SDKs independently shipbefore_step(step_name, state)over a single growablePipelineState; three implementations agreeing against one spec sentence is the sentence being wrong.before_stepis now documented as an observation hook whose return value carries no meaning. The section also showedpipeline.configureas an array of{name, …}objects — it is an object map keyed by step name, per$defs/PipelineConfigand all three parsers — and documented apipeline.step_middleware:config section that no SDK has ever parsed. -
Three SDKs emitted three different wire codes for the same pipeline-config failure.
conformance/fixtures/pipeline_failfast_config.jsonassertederror_type: "ConfigurationError"— a class name that all three happen to share — and so reported green while Python emittedPIPELINE_CONFIGURATION_ERROR, TypeScriptPIPELINE_CONFIG_INVALIDand RustCONFIGURATION_ERROR(a string in no registry). The fixture now asserts the wire code. The canonical code isPIPELINE_CONFIGURATION_ERROR;features/error-system.mdadditionally had two rows describing the same event, and now states the deliberate layer boundary: the strategy API raisesSTEP_NOT_FOUND/PIPELINE_STEP_NOT_FOUND, and thepipeline:config layer re-classifies that miss asPIPELINE_CONFIGURATION_ERRORso the message can name the offending config key. -
validate_inputis not a step. The built-in steps arecontext_creation,call_chain_guard,module_lookup,acl_check,approval_gate,middleware_before,input_validation,execute,output_validation,middleware_after,return_result. Three fixtures (pipeline_hardening,pipeline_failfast_config,pipeline_step_middleware) referencedvalidate_input, which exists in no SDK. Inpipeline_failfast_configthis was load-bearing: the fail-fast case relied on one valid key and one invalid one, so with both invalid it would have raised on the first and never reached the assertion it was written to make. -
features/observability.mdStorageBackendexamples stored bytes; the contract is JSON objects. All three Redis examples usedbytes/Uint8Array/&[u8]while the actual signatures aredict/Record<string, unknown>/serde_json::Value, and all three collectors index into the returned value — so a backend copied from the docs breaks at the collector, not at the backend. The examples now serialize at the boundary, the value type is stated normatively, and the Rust example gains the#[async_trait]andDebugthe real trait requires (it could not have compiled). -
OverridesStorewas specified with a per-key surface no SDK implements.features/system-modules.mdrequiredset(key, value)/get(key)/get_all()/delete(key); all three SDKs ship the whole-mapload()/save(mapping)of decision D-47, and thesystem.control.*paths do a read-modify-write. Spec andconformance/fixtures/overrides_store.jsonaligned to D-47. -
conformance/fixtures/async_task_evolution.jsonreaper case was arithmetically unreachable. Withnow_timestamp: 1700003000.0the cutoff expired both tasks, making the assertedremaining_task_ids: ["fresh-task-001"]impossible; apcore-typescript had silently substituted its ownstableNowto get the driver to pass. Corrected to1700002000.0. -
conformance/fixtures/event_naming.jsonstill required removed behavior. Two cases asserted the v0.21.x legacy dual-emission that v0.22.0 removed (apcore#78). Replaced with the inverse assertion — legacy names MUST NOT be emitted — so the removal is pinned rather than merely un-tested. The health-threshold case also asserted an exactp99of6000.0; p99 is a bucketed estimate and Rust's default bucket boundaries have no6.0sedge, so it now asserts a lower bound. -
schemas/apcore-config.schema.jsonrejected the specification's own canonical config.ExtensionsConfigdeclaredadditionalProperties: falseas a sibling ofoneOfwith no siblingproperties, so under Draft 2020-12 it saw no annotations from the branches and rejected everyextensionskey — includingroot, a MUST field. Replaced withunevaluatedProperties: false, which is exactly the rule type-mapping §17.2/§10 now states. The root object also omitted four documented namespaces (bindings,sys_modules,stream,obs) andExecutorConfigomitteddefault_timeout/global_timeout, all of which are normatively required;schema.validation.*was still declared;max_ref_depthallowed 128 against the §9.1.1 range of 1..100. -
observability.{tracing,metrics}.enableddefault corrected tofalse.apcore-config.schema.jsonand the §9.1 example saidtrue;defaults.schema.jsonand all three SDKs usefalse. Aligned to the implementations. -
§9.1.1 declared two configuration keys that exist nowhere.
observability.enabledandobservability.exporterappear in neither the §9.1 example nor either JSON Schema, both of which areadditionalProperties: false— so a config following that MUST was rejected. Replaced with the five nested keys actually defined. -
A05
resolve_refpseudocode still threw on every revisited$refin both PROTOCOL_SPEC §4.11 and ALGORITHMS, contradicting the new §4.15 and making the spec's ownTreeNodeexample raise. Rewritten with thevisited_refsroot-alias seed, thedepthcap and thefrom_ref_chaindiscriminator that all three SDKs implement. The §4.15 edge-case table also still requiredSCHEMA_CIRCULAR_REFfor depth exhaustion; it now requiresSCHEMA_MAX_DEPTH_EXCEEDED, which is added to the §8 error-code registry, the retryability table and the exception hierarchy along withSCHEMA_UNION_NO_MATCH/SCHEMA_UNION_AMBIGUOUS(asserted by fixtures but previously absent from both registries). -
Stale propagation of the schema-hardening change.
docs/features/schema-system.mdData Flow still saidRefResolverinlines all$reftargets; its "Semantic Format Mapping" section still demanded assertiveformatchecking;docs/guides/troubleshooting.mdgave the now-legal self-reference as theSCHEMA_CIRCULAR_REFtrigger;docs/guides/schema-definition.mdpromised native-typeformatmappings no SDK performs. -
docs/spec/conformance.md§8.1 fixture inventory was stale by 15 fixtures and 267 cases (370/43 → 637/58); regenerated from disk.conformance/README.mdlisted A23 as a coverage gap in the same commit that added its fixture, said "Four fixtures" for what is now seven non-standard patterns, omitted the three new fixtures'expected_valid/expected_featuresshapes and the root-leveldriver_contractconvention, and cited PROTOCOL_SPEC §6.2/§6.6 where DECLARATIVE_CONFIG_SPEC was meant. -
~120 non-runnable code examples across
docs/features/,docs/guides/anddocs/getting-started.md. Whole sections documented a client surface that exists in no SDK (configure_observability,use_step_middleware,client.use(mw, allow_duplicate=…),RedisTaskStore), written identically into all three language tabs — mutually consistent and mutually wrong, which is why cross-language review never caught them. Also fixed: ~25apcore-js/<subpath>imports (the package exports only.and./context-keys), 11 imports of a nonexistentapcorenpm package,Context.create()called with an options object or the wrong positional slot in all three languages, 49 Rust public fields called as methods, nonexistentErrorCodevariants, 7 Python imports from module paths that do not exist, TypeScript type-only exports instantiated withnew,Executor.callawaited in Python where it is synchronous, and agetting-started.mdRust tab that could not compile. -
RFC 2119 keywords lowercased in §13 Compatibility Rules and the Phase A approval conformance requirement.
-
ExecutionPolicystatus said the TypeScript and Rust rollout was in progress; both shipped in 0.26.0. -
The conformance inventory's case counts were unverified and 8 of 60 had drifted.
docs/spec/conformance.md§8.1 prints a per-fixture case count and a Total; CI checked only that each fixture name appeared.config_key_governancesaid 4 against 6,redaction_config4 against 7,pipeline_step_middleware6 against 9,event_naming8 against 7 — drift in both directions — and the Total read 646 against an actual 657. Counts corrected, and the CI step now verifies each row and the Total against the fixtures. Injection-tested in both directions: altering a row and altering the Total each turn the step red. A number nobody checks reads as coverage in every review and every inventory built from it, which is the same failure theexpected-keys guard exists to catch, one level up.
-
Two fixture cases stated expectations no driver could assert (#92).
identity_system'sidentity_propagates_to_child_contexthadexpected: "child.identity === parent.identity"— prose in a value slot. A driver cannot compare that to anything, so every driver hardcoded the comparison and the fixture value was decoration; it now states four checkable fields.context_create's rebind case is covered above. Both were found bycheck_case_pinning.py, which reported them as run by nobody. -
Driver contracts added to the three fixtures whose declared values reached no assertion (#92).
error_codes,version_negotiationandcall_chainnow state, in the fixture itself: assert the WIRE CODE not the class name; branching on"expected_error" in casetests that the key exists, not what it says; an expectation the driver does not recognise MUST be a hard failure rather than a skipped branch; and a positive case whose expectation is the sentinel"ok"MUST assert an observable post-condition, because "the call did not raise" is also satisfied by an implementation that does nothing.call_chainis cited as the model — its negative half maps the wire code through a table before asserting, which is exactly why its negative cases are pinned and its positive ones are not. The same file contains both the right and the wrong pattern. -
check_case_pinning.pyhad three bugs that manufactured its own findings, each an instance of the class it exists to detect.mutate({})returned{}, so a case expecting an empty object could never go red.cargo test -- <name>filtered by test name, matched nothing, exited 0, and a run that executed no tests was read as "nothing went red" — every Rust verdict in the first per-SDK sweep came from that. Fixing it introduced the third: a combined--test A --test B -- filterapplies the filter to both binaries, silently filtering out the real driver. The tool now raisesNoTestsRaninstead of treating a no-op run as green. Corrected sweep: 651 cases mutated, 23 unpinned in 5 fixtures, down from a claimed 25 in 6 —dependency_version_constraintsturned out to be fully pinned by apcore-rust, a finding the broken invocation had buried.
PROTOCOL_SPEC bumped to v1.9.0-draft.
-
Execution Policy — first-class external governance (PROTOCOL_SPEC §7.9, #76). New normative section defining a declarative, execution-time policy layer that overrides a module's
requires_approval/destructiveannotations independently of how the module was registered. Normative rules: attach at the Executor and consult from the Approval Gate (Step 5); pattern rules matched with ACL wildcard semantics (A08) and specificity scoring (A10), with the more-restrictive rule winning ties; policy overrides take precedence over declared annotations;gate_destructiveopt-in resolves thedestructive→approval footgun; the handler-visibleApprovalRequest.annotationscarries effective values (preserving the §7.3 "requires_approval guaranteed true" contract); fail-loud principle — a needs-approval-but-no-handler case MUST warn (default) or fail closed understrict, and policy documents MUST reject unknown keys. Adds Conformance Level 4 (Governance). Reference pilot: apcore-python 0.26.0; TS/Rust rollout in progress. -
Governance events on the event bus (PROTOCOL_SPEC §9.16.2, #77). Three new canonical event types make the ACL → policy → approval chain observable:
apcore.approval.decision(every adjudication incl. strict fail-closed;infofor approved/pending,warnfor rejected/timeout),apcore.policy.override(when a policy changes effective governance), andapcore.acl.denied(on ACL denial). All are emitted only when an event emitter is configured, are best-effort side channels, and are suppressed on a skipped gate / dry-run preflight. -
Documented six already-emitted events that were absent from the §9.16.2 canonical table:
apcore.stream.post_validation_failed,apcore.registry.module_load_failed,apcore.circuit.opened/apcore.circuit.closed,apcore.subscriber.circuit_opened/apcore.subscriber.circuit_closed, plus the dead-letterapcore.event.delivery_failed. The table previously claimed to list "all events emitted by apcore SDKs" but omitted these.docs/features/event-system.mdmirrors the additions.
- SDK-side companion work (not spec changes), all tracked in their own changelogs:
- #76/#77 pilots landed in apcore-python, apcore-typescript, and apcore-rust at 0.26.0 (
ExecutionPolicy+ the three governance events + the no-handler-skip → fail-loud flip). - #78 (event hygiene): apcore-python and apcore-rust dropped the legacy unprefixed dual-emission (
module_registered/error_threshold_exceeded/ …) to comply with the v0.22.0 canonical-only MUST; apcore-typescript was already compliant. apcore-rust additionally now actually emitsapcore.stream.post_validation_failed(previously a comment-only stub).
- #76/#77 pilots landed in apcore-python, apcore-typescript, and apcore-rust at 0.26.0 (
-
Config-driven ACL discovery —
acl.rootactivation (#74, D-64).acl.rootwas a dead config key in all three SDKs: registered (hard-required in Rust) and validated, but never consumed to load an ACL. A newACL.discover(config)resolvesacl.rootand attaches an ACL only when the path exists, wired automatically by theAPCorebootstrap.acl.rootis a directory by convention — discovery loads<root>/global_acl.yaml(PROTOCOL_SPEC §3.1) — and MAY also point directly at a YAML file. Critical non-breaking invariant: a missingacl.rootpath attaches no ACL (preserving today's no-enforcement default) and MUST NOT synthesize an emptydefault_effect: denyACL. Discovery is skipped when the caller supplies their ownExecutor, so an explicitly-wired ACL is never clobbered. New conformance fixtureconformance/fixtures/acl_root_discovery.jsonlocks the cross-language contract; theACL.discovercontract is documented indocs/features/acl-system.md. Implemented acrossapcore-python,apcore-typescript, andapcore-rust. -
API Surface & Naming Conventions specification (
docs/spec/api-surface-conventions.md). Normative rules for when a symbol is public API, why cross-boundary contract members MUST NOT carry a private name, and the per-language visibility-vs-discoverability idioms. Includes §8 Cross-SDK Surface Equivalence: the structural divergences that are by-design (error-taxonomy shape — per-type classes vs. a singleErrorCodeenum; namespace depth — sub-package vs. root-flattened) and the reachability rule that still flags a genuine break (a public symbol reachable only through an internal implementation module), plus auditing guidance so these no longer surface as false-positive findings. -
RFC —
include:cross-file configuration composition (docs/spec/rfc-config-include.md, Proposed) + decision D-65 (#75). Design-first contract for a top-levelinclude:key: file-path list resolved relative to the declaring file, pre-validation expansion into one merged mapping, precedenceA < B < local(deep-merge mappings, replace scalars/lists), recursive includes with cycle detection (CONFIG_INCLUDE_CYCLE). YAGNI exclusions: no globs, remote URLs, conditional includes, or list-element merging. No SDK implementation yet — awaiting maintainer ratification.
-
acl.rootdefault unified to./aclacross all three SDKs (#74, D-64). Rust previously hard-requiredacl.rootand rejected its omission withCONFIG_INVALID; it now defaults to./acllike Python and TypeScript, so a config omitting the key is valid in every SDK. Removes a cross-language divergence where the sameapcore.yamlpassed in Python/TS but failed validation in Rust. -
docs/features/acl-system.md— added theACL.discovercontract (inputs, ordered side-effects, the missing-path danger admonition, and cross-language usage showingacl.rootinapcore.yamlwith automatic attach viaAPCore).
- Decision log: D-64 (
acl.rootactivation) recorded and implemented; D-65 (include:) opened as a design-first RFC awaiting ratification before any code. Front-matter status is now 51 resolved / 8 open. - SDK-side companion fix (not a spec change):
apcore-pythonresolved #30 by re-exporting the registry module-id constants (MAX_MODULE_ID_LENGTH,RESERVED_WORDS,REGISTRY_EVENTS,EPHEMERAL_NAMESPACE_PREFIX,DEFAULT_MODULE_VERSION,MODULE_ID_PATTERN) from public paths — exactly the class of break the new §8 reachability rule codifies. Tracked in theapcore-pythonchangelog.
-
Per-instance
ToggleStateisolation (#71). EachAPCoreinstance now owns oneToggleState, injected into both the toggle module (write path) and the pipeline lookup stepBuiltinModuleLookup(read path). Disabling a module on oneAPCoreinstance no longer affects another instance in the same process — closing the isolation gap for multi-tenant servers and test bleed. The process-globalToggleStateis retained only as a fallback for the freeis_module_disabled/isModuleDisabledfunction (callers that hold no instance handle). A-D-12 is re-scoped from "process-global, survives reload" to "isolated to theAPCoreinstance, survives reload of that instance" (docs/features/system-modules.md). New conformance fixtureconformance/fixtures/toggle_state_isolation.jsonlocks the cross-language contract (cross-instance isolation + reload survival). Implemented acrossapcore-python,apcore-typescript, andapcore-rust. -
Canonical AI-agent tool-governance ACL artifact + fixture (#72). New runnable reference
examples/acl/agent-tool-governance.yaml— adefault_effect: denypolicy that scopes tool access by caller pattern + identityroles+max_call_depth:@external→executor.*.readonly;agent.*+roles: [reader]→executor.*.read/executor.*.querycapped atmax_call_depth: 3;agent.*+roles: [data_admin]→data.export/executor.*.delete(uncapped). New conformance fixtureconformance/fixtures/acl_agent_scoping.jsonfreezes the scenario as a cross-language contract (19 cases: the@external < reader < data_adminprivilege gradient, the inclusive depth boundary — depth 3 allowed / 4 denied, role separation, and missing-identity deny). New guide section "9.4 AI Agent Tool Governance" indocs/guides/acl-configuration.md. Additive and non-normative — noprotocol-spec.mdchange; the ACL engine already supportsconditions: {roles, max_call_depth}. Framework integrations (django-apcore,fastapi-apcore, …) vendor the artifact and verify identical decisions.
docs/features/system-modules.md— toggle-state contract re-scoped from process-global to per-instance (#71). The "Disabled modules" note, thesystem.control.toggle_featurepostconditions, and theis_module_disabledContract block now state that toggle state is isolated to the owningAPCoreinstance (with the standalone free function reading the global fallback). No public method-signature changes.
docs/features/event-system.md—EventEmitter.emitContract internal contradiction resolved. The### Inputsbullet saidevent_type"MUST NOT be empty" while the same block's### Errors("No errors raised to the caller. emit is fire-and-forget") and### Properties("never raises") sections — which all three SDKs implement — mandate never-raising. Surfaced by a cross-languagetester --category protocolrun (a green TypeScript placeholder test mislabeledSKIPhad been miscounted as enforcing the rule). TheMUSTon the caller is preserved, reframed as a caller precondition, not a validated rejection, matching theErrors/Propertiessections and the unanimous SDK behavior. No public surface change, no normative weakening, version stays0.24.0. The correspondingapcore-typescriptplaceholder test was converted to a realit.skip()so it stops registering as a false-positive pass.
- #70 (default AI error-recovery metadata) confirmed complete across all three SDKs.
user_fixable-by-code resolution and theai_guidancedefaults ship inapcore-python/apcore-typescript/apcore-rustand passerror_recovery_metadata.json(the 0.23.0 "apcore-typescript / apcore-rust pending" note is superseded). Remaining follow-ups are tracked outside this changelog entry: theprotocol-spec.md§8user_fixablealignment note (requires a linked issue + dual maintainer review per repo policy) and the downstreamapcore-mcpfloor bump toapcore>=0.23.0.
- Conformance fixture
error_recovery_metadata.json— default AI error-recovery metadata per error code (#70). Defines the cross-language contract forretryable+user_fixabledefaults:user_fixableis resolved from the error code (caller-fixable-by-input =true; governance/system/structural/transient =false); codes whoseuser_fixableisnullare left for the module author (e.g.MODULE_EXECUTE_ERROR). All SDKs MUST produce these defaults when an error is constructed without overrides.ai_guidancetext is not pinned (human-readable, may carry runtime context). Registered inconformance/README.md. Implemented inapcore-python0.23.0;apcore-typescript/apcore-rustpending.
error_serialization.jsonomits_null_optionalscase now uses a real policy-free code (CONTEXT_BINDING_ERROR) (#70). Real codes such asGENERAL_INVALID_INPUTnow carry a defaultuser_fixable, so the null-optional-omission case must use a code absent from the user_fixable policy — and a realErrorCodevariant, since Rust models codes as a closed enum — to keep exercising sparse-output omission across all SDKs.
These are spec-consistency clarifications surfaced by a cross-language sync audit. They resolve internal contradictions/gaps in the feature specs so the three SDKs have a single source of truth to converge on; no public surface changes, version stays 0.23.0.
- Error fingerprint definition de-duplicated (
docs/features/observability.md). The "Error fingerprinting" section defined the digest asSHA-256(error_code:top_frame_hash:sanitized_message)while §1.4 defined it asSHA-256(error_code:module_id:normalized_message). Atop_frame_hash(stack frame file/function/line) is not portable across Python/TypeScript/Rust, so it can never produce an equal cross-language fingerprint. §1.4 (the 3-part,module_id-based form) is now the single authoritative definition;top_frame_hashis demoted to an optional language-local diagnostic field that MUST NOT influence the shared fingerprint, and the normalization is pinned to the exact five-step §1.4 algorithm (no extra hex-collapsing step). Code follow-up:apcore-pythonmust drop the top-frame component and the extra normalization step to match TS/Rust. - Event-emitter overflow behavior is now normative (
docs/features/event-system.md). Theemit()contract previously left buffer-overflow behavior undefined, allowing a boundedmaxPendingqueue to silently drop deliveries (bypassing the dead-letter path).emit()now MUST NOT silently discard an accepted event: a bounded buffer's overflow MUST either apply backpressure or fail through the dead-letter path (apcore.event.delivery_failed,reason: "pending_overflow"). Code follow-up:apcore-typescriptmust routemaxPendingoverflow through the DLQ instead ofconsole.warn. Config.validate()contract added (docs/features/config-bus.md). The spec previously defined no required-field set or value constraints, so each SDK enforced a different subset (same config, three verdicts). Added a normativeContract: Config.validatewith the canonical required fields, value constraints, and namespace-mode schema/strict rules, anchored to the reference SDK. Code follow-up:apcore-rustandapcore-typescriptalign up to the full set.- Conformance fixture
multi_module_discovery.json::full_id_grammar_validcorrected. A single-class input expected the appended idexecutor.math.arithmetic.addition, contradicting the single-class identity guarantee (and thesingle_class_id_unchangedcase in the same fixture). Now expects the bareexecutor.math.arithmetic; verified green in all three SDKs. Contract: guard_call_chain### Inputscorrected (docs/features/call-chain-guard.md). The Contract block listed acontext(Context) input and named the limit paramsmax_depth/max_repeat, contradicting the function signature defined earlier in the same file and implemented identically across all three SDKs:guard_call_chain(module_id, call_chain, *, max_call_depth, max_module_repeat). The block now listscall_chain(notcontext) and the correct keyword names, making the spec internally consistent; thepureproperty note now referencescall_chainrather than "context state". No public surface changes; surfaced by a cross-languagetester --category protocolrun.
-
Decision D-24 —
Context.create()signature unified across all SDKs (#66). Reduced to the six caller-supplied fields only:identity,trace_parent,cancel_token,data,services,global_deadline. Two prior inputs are removed from the public factory surface:executor— Executor self-binds at pipeline entry under the new normative §"Contract: Executor binding to Context" (docs/features/core-executor.md). This unifies three previously distinct binding scenarios under one rule: local construction viaContext.create(), cross-process deserialize, and hot-reload survivor restore.caller_id— zero production / test / doc callers across the 25-repo ecosystem. Top-level Contexts always havecaller_id = null; the value is managed exclusively byContext.child(). Reserved name for future revisions.
Added
cancel_tokenas a first-class parameter, eliminating the post-hocctx.cancel_token = tokenanti-pattern documented in 9 production sites acrossapcore-mcp-{python,typescript},apcore-typescript/async-task.ts(which had to cast awayreadonly),axum-apcore,django-apcore,fastapi-apcore.Rust
TraceParentstruct gains atracestate: Vec<(String, String)>field to align with Python/TS shape; the redundantContextBuilder::tracestate()setter is removed.Two new normative sections clarify distributed semantics:
- §"Contract: Distributed cancellation" —
cancel_tokenis local-only; cross-process cancellation MUST go through out-of-band channels (e.g., AsyncTaskStore task_id lookup). - §"Contract:
global_deadlinedistributed semantics" —global_deadlinedoes not propagate across process boundaries; callers needing wall-clock deadline propagation SHOULD store it incontext.dataunder an extension key.
Conformance fixture
context_create.jsonvalidates cross-SDK parameter parity, removal ofexecutor/caller_id, idempotent same-executor rebinding, and cross-executor conflict behavior. -
Renumbered decision-log entries D-17–D-22 → D-58–D-63 to resolve a duplicate-ID collision with the v0.22.0 executor/async hardening decisions (which retain D-17–D-22). See
docs/spec/2026-05-decision-log.md.
-
Decision D-17 —
TaskStoreis async across all SDKs (docs/features/async-tasks.md§1.1). Pluggable backends like Redis or SQL cannot satisfy a syncTaskStorecontract without blocking the runtime's event loop. New normative: everyTaskStoremethod MUST be asynchronous in Python (async def), TypeScript (returnsPromise<T>), and Rust (async fnvia#[async_trait]).InMemoryTaskStoreMUST still expose async signatures even though its operations are CPU-only — uniform shape lets stores compose generically. Supersedes the partially-sync contract present in apcore-python and apcore-typescript through v0.21.x. Found via/apcore-skills:sync(finding A-D-AT-04). -
Decision D-18 —
cancel()MUST be a real interrupt across SDKs (docs/features/async-tasks.md§Cancellation Integration). Cooperative-flag-only cancellation is non-conforming. In TypeScript specifically,CancelTokenMUST be backed by anAbortControllerand MUST exposesignal: AbortSignalonContextso modules usingfetch/setTimeout/ Web Streams participate in real abort. Closes the cross-SDK divergence where Pythonasyncio.Task.cancel()and Rusttokio handle.abort()interrupted in-flight work but the TS flag-only path silently completed module side effects. Found via finding A-D-AT-02. -
Decision D-19 —
call_with_trace/callWithTracesharescall()error semantics (docs/features/core-executor.md§Trace Variants). The trace variant MUST run the sameon_errormiddleware chain, apply the same cancellation short-circuit (D-20), and apply the sameMiddlewareChainErrorunwrap (D-22). The trace is the observable record of execution — including any middleware recovery — not a sanitized projection. Closes the divergence where TS+Rustcall_with_tracerethrew unconditionally while Python ranon_errorrecovery. Found via finding A-D-EXEC-004. -
Decision D-20 — Cancellation short-circuits the
on_errorchain (docs/features/core-executor.md§Cancellation Short-Circuit).ExecutionCancelledErrorMUST be detected after pipeline-error unwrap and propagated directly, bypassingon_error. Rationale: cancellation is a caller-driven request to stop, not a recoverable failure; allowingon_errormiddleware to observe it lets logging middleware swallow it or retry middleware reissue aRetrySignalthat restarts the loop. Closes the gap in apcore-rust where missing short-circuit allowed middleware to recover cancellation. Found via finding A-D-EXEC-003. -
Decision D-21 — Cancel token MUST be checked at two pipeline points (
docs/features/core-executor.md§Cancel Token Mid-Pipeline Check). The pipeline MUST observecancel_tokencancellation at Step 2 (Call-Chain Guard) and again at Step 8 (Execute), in addition to honoring it insidemodule.execute()itself. Single-check implementations leak compute (the pipeline runs ACL/middleware/validation even though the caller has already cancelled) and are non-conforming. Closes the divergence where TS checked at step 2 but Python+Rust only checked at step 8 (and Rust missed step 8 entirely). Found via finding A-D-EXEC-002. -
Decision D-22 —
MiddlewareChainErrorMUST be unwrapped before propagation (docs/features/core-executor.md§Error Unwrap Rule). When a middleware (before/after/on_error) raises a domain-typed error likeApprovalDeniedError, the chain machinery may wrap it inMiddlewareChainErrorfor diagnostics. The executor MUST unwrap before propagating, surfacing the original typed cause. SDKs MUST NOT replace the cause with a genericModuleExecuteError. Closes the gap where Python wrapped toModuleExecuteErrorwhile TS+Rust unwrapped — breaking MCP/A2A bridges that key off the typed error. Found via finding A-D-EXEC-005. -
Decision D-23 —
get_status/list_tasksMUST return shallow copies in every SDK (docs/features/async-tasks.md§Contract: AsyncTaskManager.get_status). Closes the contract contradiction where the prior spec said "the live dataclass reference (Python); callers MUST NOT rely on mutation" while finding A-D-AT-06 mandated copies. Python now returnsdataclasses.replace(info), TypeScript returns{ ...info }, Rust returns a clone — uniform mutation-safety contract across all three SDKs. Found while resolving A-D-AT-06. -
docs/features/registry-system.md— Registration Ordering Invariants now explicitly apply to every register path. Added a!!! warning "Applies to every registration path"admonition clarifying the invariants apply uniformly to the publicregister()API, internal helpers (register_internalused by sys-modules), and discovery-driven paths (discover(),register_discovered, hot-reload). SDKs MUST NOT create per-path exceptions; if a discover-timeon_loadcallback needs to enumerate sibling modules, the callback MUST be re-shaped as a post-discover hook rather than as grounds for an early-visibility carveout. Closes implementation drift where apcore-python and apcore-typescript implemented deferred-publish onregister()but inserted into the visible map BEFOREon_loadon the discover path. Found via findings A-D-REG-003 and A-D-REG-004.
- §9.16.2 Canonical Core Event Types table aligned with v0.22.0 #36 rename.
docs/spec/protocol-spec.md§9.16 still listed the pre-v0.22.0 canonical namesapcore.module.registered/apcore.module.unregistered(subsystemmodule) andapcore.error.threshold_exceeded/apcore.latency.threshold_exceeded(categorieserror/latencyas the subsystem segment, which violates theapcore.<subsystem>.<event>convention). The v0.22.0 rename had shipped only indocs/features/event-system.md(legacy-aliases table + canonical-events table), leaving the normative spec table contradicting the feature spec, the conformance fixtureevent_naming.json, the registry-system / rfc-ephemeral-modules / system-modules docs, and all three SDKs' v0.22.0 behavior. Updated rows 6035-6036 toapcore.registry.module_registered/apcore.registry.module_unregisteredand rows 6040-6041 toapcore.health.error_threshold_exceeded/apcore.health.latency_threshold_exceeded; updated the §9.16.1 example row; rewrote the §9.16.2 preamble + collision-resolution note to distinguish the v0.18.0 short-form removals (Cohort A) from the v0.22.0 subsystem-segment renames (Cohort B). - User-facing examples no longer subscribe to v0.22.0-removed legacy event names.
docs/features/apcore-client.md(Python / TypeScript / Rust Production Setup tabs and theevent_typeparameter example),docs/features/event-system.md(three Subscriber examples),docs/features/observability.md("Events emitted" table), anddocs/features/system-modules.md(Configuration YAML comments) all referencedapcore.error.threshold_exceeded/apcore.latency.threshold_exceeded. Replaced with the v0.22.0 canonicalapcore.health.error_threshold_exceeded/apcore.health.latency_threshold_exceeded. The legacy names remain documented in theevent-system.mdrename table for historical reference; no other doc still teaches users to subscribe to the removed names. Found via/apcore-skills:sync --scope core(findings B-001 through B-004). - Cross-language doc/guide consistency corrections (second
/apcore-skills:sync --scope corepass).docs/guides/testing-modules.md— removed the forbiddenexecutor=argument from both the Python and TypeScriptContext.create()examples;executorself-binds at pipeline entry per D-24 / §"Contract: Executor binding to Context" and is not acreate()parameter.docs/features/event-system.md— past-tensed the legacy-event dual-emission rule: dual-emission applied through v0.21.x only and ended at v0.22.0, where implementations emit the canonical names exclusively (aligns withdocs/spec/protocol-spec.md§9.16).docs/features/registry-system.md— added the missingawaitto the async TypeScriptregistry.register()example.docs/features/middleware-system.md— corrected the stale note claiming TypeScript catchesafter()errors per-hook; all three SDKs fail-fast on the first error. - Call / stream Contract blocks now cite the registered error code
SCHEMA_VALIDATION_ERROR.docs/features/apcore-client.md(thecallandstreamContract### Errorsblocks) anddocs/features/streaming.md(theModule.streamContract) declaredSchemaValidationError(code=SCHEMA_VALIDATION_FAILED)for input-schema validation failures.SCHEMA_VALIDATION_FAILEDis not the registered code for the public call/stream surface — the canonical code perdocs/spec/protocol-spec.md§8.2 / §4.14 and conformanceT04-005/T06-006isSCHEMA_VALIDATION_ERROR, which the Python and Rust SDKs emit. Corrected all three Contract-block citations. The_FAILEDform remains intentionally documented as a validation-outcome alias indocs/features/error-system.md(Schema Edge-Case Errors table) and on the raise-on-failurevalidate_input/validate_output/validate_or_errorentry points indocs/features/schema-system.md; those are left unchanged. Found via/apcore-skills:sync --scope corereview (finding B-001). docs/features/error-system.md— reserved-error-code-prefix list de-duplicated to a single canonical set of 14. The requirements section (§"Error Code Registry") and theErrorCodeRegistrysection carried two contradicting reserved-prefix lists: the former still listed the 0.18-retiredERROR_FORMATTER_/EXECUTION_/RELOAD_/TASK_prefixes (plus an inconsistent count) while the latter listed only the 14 common-core prefixes. Both lists now declare the identical 14 (ACL_,APPROVAL_,BINDING_,CALL_,CIRCULAR_,CONFIG_,DEPENDENCY_,ERROR_CODE_,FUNC_,GENERAL_,MIDDLEWARE_,MODULE_,SCHEMA_,VERSION_). Added a normative clarification that one-off framework codes outside these prefixes (CIRCUIT_BREAKER_OPEN,CONTEXT_BINDING_ERROR,STREAMING_INTERFACE_MISMATCH,STRATEGY_NOT_FOUND,PIPELINE_*,STEP_*,RELOAD_FAILED,EXECUTION_CANCELLED,TASK_LIMIT_EXCEEDED) are protected by exact-code collision detection rather than prefix reservation. SDKs (apcore-python,apcore-rust) that had drifted to broader prefix sets are realigned to the 14 in their respective repos. Found via/apcore-skills:sync --scope core(findings B-001, A-D-006).docs/features/context-object.md— repaired malformed Rust fenced code block. The canonicalContext::create()example had an unclosedIdentity::new(...)call and two stray```rust/```fences mid-block, causing the 6-parameterContext::createexample to drop out of the rendered MkDocs page. The block is now one continuous, complete Rust example. Found via/apcore-skills:sync --scope core(finding B-002).docs/features/apcore-client.md— Global Singleton scoped to Python-only. The §"Global Singleton" SHOULD statement claimed both Python and TypeScript provide module-level convenience functions (apcore.call()); in practice only the Python SDK does. TypeScript is now grouped with Rust as explicit-instances-only, matching the shipped SDKs. Found via/apcore-skills:sync --scope core(finding B-004).
- §9.9.5 Reserved Namespace Query — new normative API requirement (#60). All SDKs MUST expose a public, read-only query API returning the set of reserved top-level namespace names (
apcore,_configat minimum). The query API is the single source of truth used byregister_namespaceto enforceCONFIG_NAMESPACE_RESERVED(§9.5.1 rules 3 and 4). Intended for third-party consumers (custom CLIs, framework integrations) that accept user-supplied namespace names and want fail-fast pre-validation. Class-level / module-level access (noConfiginstance required). Cross-language examples added for Python (Config.reserved_namespaces()), TypeScript (Config.reservedNamespaces), and Rust (Config::reserved_namespaces()). docs/features/event-system.md— Event Delivery Semantics section (#61). Normative cross-subscriber delivery contract: every subscriber type (built-in and user-registered) MUST accept aretryblock (max_attempts/initial_backoff_ms/max_backoff_ms/backoff_multiplier) governing retry on transient delivery failure; permanent failure MUST emitapcore.event.delivery_failed(with full payload schema) which itself MUST NOT be retried;subscribe()SHOULD accept an optionalon_failurecallback. New optionalidfield on every subscriber config surfaces a stablesubscriber_idin DLQ events. Thea2asubscriberskill_idis now configurable (default"apevo.event_receiver"). TheWebhookSubscribervsA2ASubscribercomparison table is updated: both now apply the unified retry policy. Discovered during apcore-a2a upgrade — closes the silent-drop gap for in-process subscribers that previously had no retry / DLQ path.docs/features/streaming.md— Streaming Module Interface section (#62). ExplicitStreamingModuleinterface replaces duck-typing: Python@runtime_checkableProtocol withisinstancedetection; TypeScript interface +Symbol.for("apcore.streaming")marker +isStreamingModuletype-narrowing helper (transitional fallback to method-presence detection with a one-shot deprecation log; marker becomes MUST at next major); Rusttrait StreamingModule: Moduleaccessed viaModule::as_streaming() -> Option<&dyn StreamingModule>, coexisting with the existingModule::stream() -> Option<ChunkStream>(the two paths MUST stay consistent per module). DefinesStreamingInterfaceErrorraised at module-load time when a declared-streaming module's signature does not match the interface. Adapter / bridge code (apcore-a2a, apcore-mcp) MUST use the standard detection mechanism, not barehasattr/typeofchecks.docs/features/context-object.md— Typed Access via ContextKey[T] section (#63). Promotes the existingContextKey[T]API (design-context-annotations-acl.md §1.4) as the recommended pattern for stable, schema-bearing state oncontext.data. Cross-language examples with the correct key-anchored API (KEY.set(ctx, value)/KEY.get(ctx, default)/KEY.exists(ctx)/KEY.delete(ctx)/KEY.scoped(suffix)) for Python / TypeScript / Rust. Aligns the third-party namespace rule with the existingext.*prefix mandated by middleware-hardening §1.1. Framework-reserved key list sourced from spec §1.5 (TRACING_SPANS / TRACING_SAMPLED / METRICS_STARTS / LOGGING_START / REDACTED_OUTPUT / RETRY_COUNT_BASE). No runtime behavior change — purely a discoverability / best-practice elevation.docs/features/middleware-system.md— Duplicate Middleware Detection section (#64). Normative SHOULD that SDKs detect prior registration of a middleware sharing the same identity (Python:f"{module}.{qualname}"; TS:"<module-specifier>:<ClassName>"with constructor-name fallback; Rust:std::any::type_name::<T>()) and emit aWARNING-level log naming both registration sites. Detection MUST be non-blocking and MUST NOT mutate registration order. Per-registrationallow_duplicateflag SHOULD be available for intentional stacking; customidentity_key(vendor-prefixed;apcore.*reserved) lets the same class be registered for distinct purposes without triggering the warning.docs/features/registry-system.md— Registration Ordering Invariants section (#65). Strong-guarantee invariant: a module MUST NOT become visible to discovery APIs (get/list/get_definition) until allon_loadcallbacks have completed successfully. On callback failure the module MUST NOT become visible and the registry MUST emitapcore.registry.module_load_failedwith{module_id, callback_name, error_type, error_message, timestamp}. The existingContract: Registry.registerside-effects ordering is updated to a deferred-publish pattern: reserve in-flight slot under registry lock → release lock → runon_loadunder per-module init lock → atomically publish or roll back. Callbacks for distinct modules MAY still run concurrently.conformance/fixtures/event_delivery_semantics.json(#61) — 4 cross-language test cases: retry succeeds before exhaustion (verifies attempt count + backoff delays), permanent failure emitsapcore.event.delivery_failedwith full normative payload, DLQ-subscriber failure is not re-retried (no infinite loop), SDK-generatedsubscriber_idwhen config omitsid.conformance/fixtures/registry_load_ordering.json(#65) — 4 cross-language test cases: visibility only after successfulon_load, callback failure blocks visibility and emitsapcore.registry.module_load_failed, concurrent same-ID rejected withDUPLICATE_MODULE_ID, concurrent distinct-IDs run in parallel (wall-clock assertion).conformance/fixtures/schema_content_hash.json(A-D-037) — 5 cross-language schema content-hash canonicalization fixtures designed to expose canonicalization divergence: float rendering (1.0), non-ASCII Unicode keys/values, large integers beyond IEEE-754 exact range, unsorted nested object keys, and a baseline control. Noexpectedhash is recorded — each SDK computes the content hash and the harness asserts cross-repo byte-for-byte agreement.
A2ASubscriberdelivery behavior — now retries on 5xx / connection / timeout (#61). Previously single-attempt: A2A failures returned silently after one try. Now A2A applies the unifiedretrypolicy (defaultmax_attempts: 3) likeWebhookSubscriber. SDK consumers that relied on single-attempt semantics MUST setretry: { max_attempts: 1 }explicitly. Side effects of retries (duplicate downstream processing on retried 5xx) are the receiver's responsibility — implement idempotency at the A2A endpoint.Registry.register— concurrent same-ID registration semantics tightened (#65). Previously a race between tworegister()calls for the samemodule_idcould resolve via lock-ordering with one succeeding and one failing late. Now an in-flight loading set rejects the second caller immediately withInvalidInputError(code=DUPLICATE_MODULE_ID). Callers that previously assumed race-tolerant insertion MUST serialize per-module-ID registration explicitly. The new behavior is symmetric with sequential same-ID registration, which has always raisedDUPLICATE_MODULE_ID.
- Issues #61–#65 were discovered during the apcore-a2a upgrade audit and target gaps that bleed into adapter / bridge implementations. They are feature-level changes;
docs/spec/protocol-spec.mdis unchanged. SDK rollout follow-ups will be filed inapcore-python,apcore-typescript, andapcore-rust. - Behavior changes (A2A retry, registry concurrent same-ID) are normative spec changes, not implementation drift. SDKs implementing v0.22.0 MUST adopt the new behavior on the next minor release; SDK CHANGELOGs SHOULD restate these
Changedentries.
- §2.5 Reserved Words —
ephemeraladded to framework reserved list (RFCrfc-ephemeral-modules.mdaccepted). New reserved namespaceephemeral.*for programmatically-generated runtime modules synthesized by LLM agents (à la ToolMaker, ACL 2025). StandardRegistry.register()only —register_internal()MUST rejectephemeral.*IDs. Reserved-namespace semantics table added below the YAML block. Seedocs/spec/rfc-ephemeral-modules.mdfor the full namespace contract (registration / lifecycle / audit / sandboxing). - §4.4 ModuleAnnotations — new
discoverable: booleanfield (defaulttrue). Controls visibility in enumeration surfaces (Registry.list(),Registry.find(), manifest export, MCPtools/list).falsehides the module from discovery while keeping it callable by exact ID.ephemeral.*modules SHOULD setdiscoverable: false. - §5.6 Module Interface Protocol — optional
preview()method (RFCrfc-preview-method.mdaccepted). Modules implementpreview(inputs, context) -> PreviewResult | nullto self-report structured-diff predictions of state changes the call would produce. Sits alongsidepreflight()(warnings) — they are orthogonal surfaces. Exception semantics mirrorpreflight()(advisory warning viamodule_previewcheck entry; does not fail validation). Pseudocode interface block +optional_methodsYAML both updated. - §12.8 PreflightResult schema — new
predicted_changes: List<Change>field;ChangeandPreviewResulttypes defined.Changehas requiredaction/target/summary(free-form strings) and optionalbefore/aftersnapshots;x-*extension fields are permitted (cross-SDK encoding patterns documented in the RFC).PreflightCheckResult.checkenum extended withmodule_preview. - §4.6 conventions table — three new
x-*AI-routing keys (D-57). Documentation-only registration of three metadata conventions surfaced by the 2025–2026 LLM-agent / tool-use frontier-research alignment audit:x-reasoning-demand(low|medium|high) — hint of the minimum reasoning capability the calling agent needs; consumed by upstream model routers for tier selection. Aligns with RouteLLM (ICLR 2025), xRouter (arXiv 2510.08439), and Cost-Aware Model Orchestration (arXiv 2512.01099).x-required-context-keys— array ofcontext.datakey names the module reads, enabling orchestrators to inject required state ahead of the call. Does not include framework-owned Context fields (trace_id,caller_id, etc.).x-supports-dry-run(boolean) — module-level signal thatExecutor.validate()(§12.2) is meaningful for this module.
docs/spec/2026-05-decision-log.md— D-57 (§4.6 reasoning/context/dry-run conventions) — marked resolved. Resolution status block updated to include D-57.
- No SDK behavior change for §4.6 D-57 conventions. All three SDKs treat module
metadataas a free-form dict /Record<string, unknown>/serde_json::Value; the framework explicitly does not validate metadata content (§4.6 "Metadata Design Principles"). Newx-*keys are additive conventions only. - Strict-mode export unaffected by D-57. §4.16 mandates that
to_strict_schema()strips allx-*extension fields, so the new keys do not surface in strict-mode output. - SDK rollout for §2.5 / §4.4 / §5.6 / §12.8 spec promotions is synchronized. All three SDKs ship the full v0.21.0 surface (Stage 2
Module.preview()+ Stage 3ephemeral.*namespace +discoverableannotation + audit single-emit +register_internal()rejection):apcore-pythonv0.21.0 — pyproject.toml bumped; PR #26 + iter-11 alignment shipped Stage 3; commit203a9a6shipped Stage 2.apcore-typescriptv0.21.0 — package.json bumped; PR #29 + iter-11 shipped Stage 2 (with TypeBoxType.Unsafeform forChange.x-*); commit577b09bshipped Stage 3.apcore-rustv0.21.0 — Cargo.toml bumped; PR #25 shipped Stage 2 prerequisite (#[non_exhaustive]hygiene); commitsafb6e05+e6abb7bshipped Stage 2Module::preview()+ Stage 3 ephemeral/discoverable.
- Conformance fixture
annotations_extra_round_trip.jsondeferred. Perrfc-ephemeral-modules.md"Transitional fixture handling during multi-SDK rollout", the fixture is NOT updated to requirediscoverableuntil all 3 SDKs have shipped support. SDKs implementingdiscoverableMAY make their conformance test runner pilot-tolerant in the interim. Synchronized fixture update will land in a follow-up PR after TypeScript and Rust shipdiscoverable.
UsageExporterProtocol/interface/trait +NoopUsageExporter+PeriodicUsageExporterin all 3 SDKs (#45 §3, D-55). Push-style usage summary export distinct from pull-stylePrometheusExporter.PeriodicUsageExporterpollsUsageCollector.summary()everyinterval_seconds(default 3600 = 1 hour) and callsexporter.export(summary).stop()halts the loop, awaitsexporter.shutdown(), and is idempotent. apcore SDKs do not ship transport-bound exporters (HTTP, Kafka, etc.) — those are explicitly out-of-tree. Documented indocs/features/observability.md"## UsageExporter (push-style)". Conformance:conformance/fixtures/usage_exporter.json(3 cases).- TypeScript registry
_filterIdConflicts8-stage decomposition matches Rust + Python (D-32, D-56). TypeScript_discoverDefaultrefactored to expose_filterIdConflictsas a separate private helper between candidate enumeration and registration;_registerInOrderreduced to pure registration. Cross-language conformance no longer special-cases TS — all 3 SDKs follow the canonical 8-stage Algorithm A04 shape. conformance/fixtures/usage_exporter.json(#45 §3, D-55) — 3 cross-language test cases:NoopUsageExporterno-op,PeriodicUsageExporterpushes summary at interval,stop()is idempotent and drains in-flight exports.conformance/fixtures/sensitive_keys_default.json(#43 §5, D-54) — 4 cross-language test cases: canonical 16-entry default list shape, legacy_secret_*prefix redaction under default, common credential-term redaction (case-insensitive substring), operator override replaces (does not merge) the default.docs/spec/2026-05-decision-log.md—## D-54 — sensitive_keys canonical default list,## D-55 — UsageExporter push interface (#45 §3),## D-56 — TS _discoverDefault 8-stage refactor— all marked resolved. Resolution status block updated to reflect D-30, D-31, D-32, D-54, D-55, D-56 as resolved.docs/features/async-tasks.md—Contract: ReaperHandle.stopblock (audit N-001) — Normative cross-language declaration thatReaperHandle.stop()MUST be async in all three SDKs (async defin Python,Promise<void>in TypeScript,async fnin Rust) with drain semantics — cancel + await termination — andidempotent: true.conformance/fixtures/trace_context.json(#35) — 8 cross-language test cases for the TraceContext W3C alignment: orderedtracestateroundtrip, 32-entry cap, malformed-entry tolerance, case-insensitiveTraceparent/TRACESTATEheader lookup, dynamictrace_flagshonoring on extract→inject, acceptedparent_idoverride (^[0-9a-f]{16}$), andINVALID_PARENT_IDrejection of malformed overrides.docs/features/event-system.md— Event Naming Convention section (#36) — Normativeapcore.<subsystem>.<event>form for framework-emitted events, glob-subscription examples (apcore.registry.*/apcore.health.*) across Python/TypeScript/Rust, and a deprecation table mapping legacy names (module_registered,module_unregistered,apcore.error.threshold_exceeded,apcore.latency.threshold_exceeded) to their canonical replacements with v0.22.0 removal target. Adds a "Configuration-driven subscribers" section listing all five built-in factories (webhook,a2a,file,stdout,filter) with a multi-subscriber YAML example.docs/features/system-modules.md— Contextual Auditing subsection (#45.2) — Normative rule that control modules (update_config,toggle_feature,reload_module) MUST includecaller_idand (when present) a redactedidentitysnapshot in their emitted audit events;caller_iddefaults to the literal string"@external"when unauthenticated;x-sensitiveIdentity fields are replaced with"<redacted>". Cross-language Python/TypeScript/Rust examples of the audit event payload shape.- Public SubscriberFactory API in Python SDK (parity with TS+Rust) (#36).
conformance/fixtures/event_naming.json(#36 / D-34) — 8 cross-language test cases: canonicalapcore.registry.module_registered/apcore.registry.module_unregistered, dual-emit of legacy names withdeprecated:trueduring v0.21.x, glob subscription matching forapcore.registry.*andapcore.health.*, canonicalapcore.health.error_threshold_exceeded/apcore.health.latency_threshold_exceeded, and cross-subsystem glob isolation.conformance/fixtures/contextual_audit.json(#45.2 / D-35) — 7 cross-language test cases:caller_idpropagation inapcore.config.updated,@externaldefault fornull/emptycaller_id, redactedidentitysnapshot inclusion forapcore.module.toggled,x-sensitivefield redaction (e.g.,bearer_token→<redacted>),caller_id+identityinapcore.module.reloaded, and audit event emission even when noAuditStoreis configured.docs/spec/2026-05-decision-log.mdD-34 (event naming canonicalization) and D-35 (contextual auditing for control plane) — both marked resolved with action: rename + dual-emit during v0.21.x for D-34; payload extension for D-35.- Pipeline
StepMiddlewareextension point in all 3 SDKs (#33). Lifecycle-shaped API (before_step/after_step/on_step_error) mirroring module-levelMiddleware— onion ordering, first-recovery-wins on errors, async support across Python/TypeScript/Rust. Documented indocs/features/middleware-system.md"Pipeline Step Middleware (Issue #33)" with three contract blocks. Conformance:conformance/fixtures/pipeline_step_middleware.json(6 cases). - Python
BatchSpanProcessorfor non-blocking span export (#43, parity with TS+Rust). Cross-SDK parity contract — identical default tunables (max_queue_size=2048,max_export_batch_size=512,schedule_delay_ms=5000,export_timeout_ms=30000), identicalon_end/force_flush/shutdownlifecycle, identical drop-on-full-queue semantics. Documented indocs/features/observability.md"Batch span processing" section. docs/spec/2026-05-decision-log.md— D-36 (Pipeline StepMiddleware), D-37 (Pipeline configuration fail-fast), D-38 (BatchSpanProcessor cross-SDK parity), all resolved.StorageBackendtrait/interface in all 3 SDKs withInMemoryStorageBackenddefault;ErrorHistory,UsageCollector,MetricsCollectoraccept injected backend (#43).OverridesStoreandFileOverridesStorein TypeScript SDK (parity with Python+Rust) (#45.1).Registry.discover_multi_class/Registry.discoverMultiClassmethod in Python+TS (D-15).docs/features/observability.md—## Pluggable storage backendsand## ErrorHistory eviction performancesubsections documenting the cross-SDKStorageBackendsurface,InMemoryStorageBackenddefault, out-of-tree policy for Redis/Postgres/S3, and the lazy-deletion semantics of the O(log N) min-heap eviction.docs/features/system-modules.md—## Persistent Overrides — pluggable OverridesStoresubsection coveringOverridesStoreparity across all 3 SDKs, with cross-languageFileOverridesStorewiring during APCore construction and the missing-path-on-first-run guarantee.docs/features/multi-module-discovery.md— UpdatedRegistry.discover_multi_classContract block to explicitly document that all 3 SDKs expose the discovery routine as a method onRegistry; added a per-SDK method/internal-helper mapping table and cross-language usage examples (D-15).docs/features/async-tasks.md— Notes documenting the PythonTaskStore.put → saverename + deprecation alias (D-10), the PythonTaskInfo.attempt_number → retry_countrename + deprecation property (D-13), removal of Python'sTaskStatus.RETRYING(D-12), and the RustRetryConfig::default().max_retries: 3 → 0alignment (D-14).docs/spec/2026-05-decision-log.md—## Resolution status — 2026-05-03 addendummarking D-10, D-12, D-13, D-14, D-15, D-58, D-25, D-27, and D-28 resolved; added new entries## D-39 — StorageBackend cross-SDK abstraction(resolved) and## D-40 — TS overrides persistence parity(resolved).conformance/fixtures/storage_backend.json— 5 cross-language test cases coveringStorageBackendsave+get round-trip, list-with-prefix filtering, idempotent delete, namespace isolation, and save-overwrites semantics.conformance/fixtures/overrides_store.json— 5 cross-language test cases coveringOverridesStore: save persists across reopen, startup applies overrides after base config, in-memory store for tests, missing path on first run is OK, delete idempotency.- Granular reload via
path_filterinput inReloadModuleacross all 3 SDKs (#45.4). - Rust
Config::reload_from_disk()for refreshing static config without binary restart (#45.5). - Error fingerprinting in
ErrorHistoryacross all 3 SDKs — dedup by (error_code, top-frame hash, sanitized message template) instead of exact message (#43 §4). - Configurable redaction via
obs.redaction.regex_patternsandobs.redaction.sensitive_keysConfig keys (#43 §5).
- Default
obs.redaction.sensitive_keysredaction list expanded to canonical 16-entry superset across all 3 SDKs (#43 §5, D-54). Default now ships as["_secret_*", "password", "passwd", "secret", "token", "api_key", "apikey", "apiKey", "access_key", "private_key", "authorization", "auth", "credential", "cookie", "session", "bearer"]. The leading_secret_*glob preserves the legacy_secret_-prefix behavior under the canonical default; the redundantapiKey/apikeypair is intentional so cross-SDK fixtures byte-match. Operator overrides viaobs.redaction.sensitive_keysinapcore.yamlreplace (do not merge with) the default. Documented indocs/features/observability.md"## Canonical defaultsensitive_keys". - Trace context (#35) —
docs/features/observability.mdW3C Trace Context section addstracestatepropagation (ordered, 32-entry cap, malformed-entry tolerance), case-insensitive header lookup (Traceparent/TRACEPARENT/traceparentall match), dynamictrace_flagshonoring (sampling flag from incoming request, NOT hardcoded), and an optionalparent_idargument oninject()validated against^[0-9a-f]{16}$withINVALID_PARENT_IDrejection. Cross-language Python/TypeScript/Rust examples added. docs/features/async-tasks.mdReaper example (sync B-004 + audit N-001) — Python tab now uses the canonical syncstart_reaper(ttl_seconds=, sweep_interval_ms=)signature returningReaperHandle, andawait reaper_handle.stop()for graceful shutdown (was previously syncstop()). TS and Rust tabs unchanged.docs/features/observability.mdPrometheus example (sync B-005) — All three tabs show wiring ofUsageCollectorintoPrometheusExporter(Pythonusage_collector=parameter, TS options-object, Rustwith_usage_collectorbuilder), with an admonition explaining the cross-language constructor-vs-builder shape difference.docs/features/multi-module-discovery.mdPython import path (sync B-001 / B-002) — Correctedfrom apcore.discovery import multi_classtofrom apcore import multi_class, removing an internal contradiction with the rest of the document. The TS tab now references the existingmultiClass()decorator.- Event names normalized to
apcore.<subsystem>.<event>form (#36). Legacy names dual-emitted; removal target v0.22.0. - Control-plane modules (
update_config,toggle_feature,reload) now includecaller_id+identityin audit events (#45.2). - Pipeline configuration is now fail-fast: missing step references and unmet
requires/providesraise errors instead of logging warnings (#33).ConfigurationErroris raised at YAML parse time for unknown step names inpipeline.configure[]andpipeline.step_middleware[].PipelineDependencyErroris raised at strategy construction time for unsatisfied capability declarations. Documented indocs/features/middleware-system.md"Configuration safety" subsection. Conformance:conformance/fixtures/pipeline_failfast_config.json(4 cases). ErrorHistoryeviction is now O(log N) via a min-heap (#43).- Python
TaskStore.put→save(deprecated alias retained);TaskInfo.attempt_number→retry_count(deprecated alias retained);TaskStatus.RETRYINGremoved (D-10, D-12, D-13). - Rust
RetryConfig::default().max_retriesis now0(was3) (D-14). - Rust streaming chunk merge raises
STREAM_CHUNK_NOT_OBJECTfor non-object chunks (D-58). - Rust
update_configraisesCONFIG_KEY_RESTRICTEDfor restricted keys (D-25). - Rust
UsageCollectornow computes trend from samples and supports period filter (D-27). - Rust
ContextLoggeroutput schema aligned with Python+TS (lowercase level, nestedextra,module_id) (D-28). - Python
start_reapernow uses(ttl_seconds, sweep_interval_ms)and returnsReaperHandlematching TS+Rust; old kwargs aliased with DeprecationWarning (D-11). OverridesStoreis now a pluggable trait/interface/protocol in all 3 SDKs (#45.1, D-47);FileOverridesStoreandInMemoryOverridesStoreship as defaults.- Reaper default
sweep_interval_msaligned across 3 SDKs to300_000(D-48). - TypeScript
RetryConfig.computeDelay→computeDelayMs; RustRetryConfig::delay_for_attempt→compute_delay_ms(old names deprecated, removal target v0.22.0) (D-08, D-49). - Rust
inject()now propagates inboundtrace_flagsfrom context data (D-50). - Rust
inject()now returns an error on malformedparent_idoverride, matching PY+TS (D-51). - Rust pipeline
ConfigurationErroris a distinct error code fromPipelineDependencyError(D-52). - TypeScript redaction reads canonical
obs.redaction.{regex_patterns,sensitive_keys}Config keys; legacyobservability.redaction.{field_patterns,value_patterns}honored with deprecation warning (D-53).
- Cross-language naming alignment for the
context_namespacemiddleware module (audit N-003) — Python and Rust shippedContextWriter/NamespaceCheck; TypeScript shippedContextKeyWriter/ContextKeyValidation. TypeScript renamed inapcore-typescriptv0.20.x to match the Python+Rust majority; the prior TS names are retained as deprecated aliases for one release cycle. No spec text change needed — all three SDKs now agree on the canonical names. - Async
on_errormiddleware in Python+TS now detects awaitable/thenable RETURN values rather than inspecting function shape, fixing silent Promise leaks forpartial-wrapped handlers (#42).
- D-30: clarified that
pre_approval_hookis Python-only.docs/features/multi-module-discovery.mdadds a "Python-onlypre_approval_hook" subsection explaining that Python imports the file at scan time so the hook protects against arbitrary code execution; TypeScript and Rust parse static AST/source and never import code from disk for discovery, so the hook is not present in those SDKs. Cross-language tabs show the call shape in each SDK. - D-31: documented per-language file extension scanning defaults and skip patterns.
docs/features/multi-module-discovery.mdadds a "File extensions and skip patterns" subsection with a per-SDK table: Python.py(skip__pycache__/,*.pyc, leading_), TypeScript.ts/.js(skip*.d.ts,*.test.*,*.spec.*), Rust.rsonly (configurable viawith_extensions).
-
docs/spec/design-durability-boundary.md— Durability Boundary design document. Vendor-neutral architectural document enumerating apcore's stable hooks for retry/replay/workflow layers built on top (Context JSON serialization, Approval Phase B_approval_tokenretry contract, theTaskStoreinterface, the six extension points, retry-safety annotations,context.data+ §4.6x-extension mechanism), explicit non-goals (mid-pipeline checkpointing, multi-call workflow orchestration, cost governance, first-class fields for application-level concerns apcore does not consume), five concrete integration patterns (transient retry, crash-durable single-call retry, long-running pause for external decisions, cross-process invocation, logical-call deduplication), and a gap watchlist of deliberately-deferred items. Establishes that apcore is the module standard, durable execution is a runtime concern; both can coexist without apcore absorbing workflow surface. -
docs/features/system-modules.md— System Modules Hardening section (Issue #45) — Five production-hardening extensions: (1) optionaloverrides_path/ KV-store persistence forsystem.control.update_configandsystem.control.toggle_featurechanges, loaded after base config on startup via pluggableOverridesStoreinterface; (2) contextual audit trail — all state-modifying control modules extractcontext.identityand append structuredAuditEntryrecords (timestamp, action, actor_id, actor_type, trace_id, before/after change) via a pluggableAuditStoreinterface; (3) Prometheus integration forUsageCollector— five new gauges/counters (apcore_usage_calls_total,apcore_usage_error_rate,apcore_usage_p50/p95/p99_latency_ms) exported via the existing/metricsendpoint with configurableexport_timeout_ms; (4) granularpath_filterglob field onsystem.control.reload_modulefor bulk reload in dependency topological order, mutually exclusive withmodule_id(MODULE_RELOAD_CONFLICTon conflict); (5) startup failure handling —fail_on_error: bool = Falseparameter for Python/TypeScriptregister_sys_modules(),failOnError: boolean = falsefor TypeScript, andResult<(), SysModuleError>return type for Rust (never panics or returnsOption). Updatesregister_sys_modulescontract block with newfail_on_errorparameter andSYS_MODULE_REGISTRATION_FAILEDerror code. Closes Issue #45. -
conformance/fixtures/system_modules_hardening.json— 10 cross-language test cases covering: overrides persisted on update_config, overrides loaded after base config on startup, audit entry actor_id extracted from context.identity, audit entry before/after change for toggle_feature, Prometheus /metrics includes apcore_usage_calls_total, path_filter reloads matching modules in topological order, module_id + path_filter conflict raises MODULE_RELOAD_CONFLICT, fail_on_error=True raises SysModuleRegistrationError, fail_on_error=False logs and continues, and Rust register_sys_modules returns Result not Option. -
docs/features/observability.md— Observability Hardening section (Issue #43) — Six production-hardening extensions: (1) pluggableObservabilityStoreinterface withInMemoryObservabilityStoredefault and optionalRedisObservabilityStore/SqlObservabilityStorebackends injected at construction time; (2)BatchSpanProcessorfor non-blocking OTEL export with configurablemax_queue_size(2048),schedule_delay_ms(5000),max_export_batch_size(512), andexport_timeout_ms(30000) — drops spans on full queue withspans_droppedcounter; (3) O(log N)ErrorHistoryeviction using a min-heap keyed onlast_seen_atplus an O(1) module-id index, replacing the O(M) ring-buffer scan; (4) SHA-256 content-addressable error fingerprinting with UUID/ID/timestamp normalization for accurate deduplication; (5)RedactionConfigwith globfield_patternsand regexvalue_patternsapplied at log time (union with existingx-sensitiverules); (6) K8s/Prometheus integration hooks (/metrics,/healthz,/readyz) with requiredapcore_module_calls_total,apcore_module_errors_total,apcore_module_duration_secondsmetrics and ServiceMonitor annotation guidance. AddsPrometheusExporter.exportcontract block. Closes Issue #43. -
conformance/fixtures/observability_hardening.json— 10 cross-language test cases covering: default InMemoryObservabilityStore construction, BatchSpanProcessor buffering without immediate export, BatchSpanProcessor drop-on-full-queue withspans_droppedincrement, min-heap eviction of oldestlast_seen_atentry, fingerprint-based deduplication (count increment, no duplicate entry), UUID normalization producing identical fingerprints, different error codes producing distinct fingerprints, field-pattern redaction (*password*), value-pattern redaction (^Bearer .*), and Prometheus export containing all three required metric names. -
docs/features/async-tasks.md— AsyncTaskManager Evolution section (Issue #34) — Three new capability extensions: (1) pluggableTaskStoreinterface withInMemoryTaskStoredefault and optionalRedisTaskStore/SqlTaskStorebackends injected at construction time; (2) per-task retry configuration (max_retries,retry_delay_ms,backoff_multiplier,max_retry_delay_ms) with exponential backoff scheduling andFAILEDterminal state after exhaustion; (3) opt-in Reaper background task for automatic TTL-based deletion of terminal-state tasks, guarded byreaper_enabled: true. IncludesTaskStore.saveandAsyncTaskManager.start_reapercontract blocks. Closes Issue #34. -
conformance/fixtures/async_task_evolution.json— 10 cross-language test cases covering: default InMemoryTaskStore construction, custom store injection, save-and-get round-trip, list-by-status filtering, retry scheduling on first failure, backoff multiplier delay computation, max-retries-exhausted → FAILED transition, Reaper disabled by default, Reaper deletes expired terminal tasks, and Reaper skips PENDING/RUNNING tasks regardless of age. -
docs/features/schema-system.md— Schema System Hardening section (Issue #44) — Five hardening areas: (1) union type standardization — all SDKs MUST evaluate ALL branches ofanyOf/oneOf(not short-circuit on first); (2) recursive schema support via lazy$refresolution (model_rebuildin Python,Type.Recursivein TypeScript,Box<T>in Rust); (3) Rust validator parity — normative requirement to supportallOf/anyOf/oneOf/notand numerical/string constraints, withjsonschema-rsas recommended path; (4) semantic format mapping —SHOULDmapdate-time,date,time,email,uri,uuid,ipv4,ipv6to language-native types; (5) content-addressable schema cache keyed by SHA-256 of canonical JSON — deduplicates identical schemas loaded from different paths. AddsSchema.validate_union,Schema.validate_recursive, andSchema.content_hashcontract blocks. Closes Issue #44. -
conformance/fixtures/schema_hardening_union.json— 8 test cases for anyOf all-branches evaluation, oneOf exactly-one enforcement, oneOf ambiguous-match error, allOf all-satisfied and one-fails cases. -
conformance/fixtures/schema_hardening_recursive.json— 6 test cases for TreeNode$ref: "#"recursive schema at depths 1, 3, and 5; invalid child type; empty and absent children arrays. -
conformance/fixtures/schema_hardening_constraints.json— 12 test cases for minimum/maximum, exclusiveMinimum boundary, minLength/maxLength, pattern match/no-match, andnotkeyword. -
conformance/fixtures/schema_hardening_formats.json— 9 test cases for semantic format mapping across all 8 canonical formats; invalid formats produce warn_logged=true, not hard errors. -
conformance/fixtures/schema_hardening_cache.json— 5 test cases for content-hash deduplication: same-content two-paths, different-content, key-order canonical normalization, pre/post-$refresolution, empty schema. -
NestContextFactory in
nestjs-apcore(Issue #35) — NewNestContextFactoryservice (src/context/nest-context.factory.ts) reads W3Ctraceparentheader (header key normalized to lowercase),x-correlation-id/x-request-idcorrelation headers (stored incontext.data["x-correlation-id"]), andx-user-id/Authorization: Bearerfor identity. 13 unit tests covering all fixture semantics. Exported from package index. This completes the framework-factories task for Issue #35 — Django, Flask, and NestJS all now have default ContextFactory implementations. Also fixesPROTOCOL_SPEC.md §8.5error response example (trace_idupdated from dashed UUID to 32-char lowercase hex, consistent with §5.7 and §10.5 changes shipped in v0.19.0). Closes Issue #35. -
PROTOCOL_SPEC §5.16 — Pipeline Control Flow Requirements — Normative rules for fail-fast error handling, O(1) step lookups, replace semantic for step configuration,
run_untiltermination predicate, and step-level middleware ordering. Closes Issue #33. -
docs/features/core-executor.md— Pipeline Hardening section — Implementation guidance for all 5 hardening areas (fail-fast, replace semantic, step-level middleware,run_until, O(1) lookups) with cross-language Python/TypeScript/Rust examples and aPipeline.configure_stepcontract block. -
conformance/fixtures/pipeline_hardening.json— 5 test cases for fail-fast, continue-on-ignored-error, replace-semantic,run_untilearly termination, and O(1) lookup verification. -
docs/features/event-system.md— Event Management Hardening section — Cross-language SubscriberFactory parity (TypeScript + Rust examples), three new built-in subscriber types (file,stdout,filter), circuit-breaker resilience spec withOPEN/CLOSED/HALF_OPENstate machine,apcore.subscriber.circuit_opened/circuit_closedevents,SubscriberCircuitBreaker.on_failurecontract. Closes Issue #36. -
conformance/fixtures/event_management_hardening.json— 10 test cases for SubscriberFactory registration, built-in subscriber types, filter pass/discard, circuit-breaker state transitions. -
PROTOCOL_SPEC §2.1.1 — Multi-Class Discovery — Opt-in mode allowing multiple Module classes per file. ID is derived as
base_id.snake_case(ClassName). Single-class files produce unchanged IDs (backward-compatible). IntroducesMODULE_ID_CONFLICTerror for classes that produce the same snake_case segment. Closes Issue #32. -
docs/features/multi-module-discovery.md— Full feature spec with discovery algorithm, conflict detection, backward compatibility guarantees, and cross-language usage examples. -
conformance/fixtures/multi_module_discovery.json— 8 test cases for ID derivation, snake_case conversion, conflict detection, and backward compatibility. -
docs/features/middleware-system.md— Middleware Architecture Hardening section — Context namespacing rules (_apcore.*vsext.*),CircuitBreakerMiddlewarespec (per-module error tracking, rolling window, OPEN/HALF_OPEN/CLOSED state machine,CircuitBreakerOpenError,apcore.circuit.opened/closedevents),TracingMiddlewarespec (OTLP-compatible span lifecycle, graceful no-op when OpenTelemetry is absent), YAML-driven declarative middleware configuration (tracing,circuit_breaker,logging,customtypes), and async handler detection fix (inspect.iscoroutinefunctionin Python,handler.constructor.namein TypeScript). IncludesMiddleware.detect_asynccontract block. Closes Issue #42. -
conformance/fixtures/middleware_hardening.json— 10 test cases covering context namespace validation (valid_apcore.*, validext.*, violation), circuit breaker state transitions (opens at threshold, short-circuits in OPEN, probes in HALF_OPEN, closes on success), tracing span creation and no-op without OTel, and async coroutine detection correctness.
- PROTOCOL_SPEC §7 — Approval Phase B Resume semantics clarified. Added a normative paragraph after the Step 5 Algorithm formalizing existing reference behavior: when a caller retries an
APPROVAL_PENDINGcall by injecting_approval_tokenintoarguments, the executor MUST re-enter the pipeline from Step 1 with no preserved intermediatePipelineContextstate. Pre-approval middleware side effects re-execute on resume; middleware needing at-most-once semantics across an approval gate SHOULD inspect_approval_tokenitself. No behavioral change — documents existing implementation. Cross-referencesdocs/spec/design-durability-boundary.md§2.2.
docs/features/async-tasks.md— TypeScriptawaitmissing on async methods. The TypeScript tab was callingmanager.submit(),manager.cancel(), andmanager.shutdown()withoutawait, while the Python and Rust equivalents correctly usedawait/.await. All three calls are now correctly awaited.docs/features/event-system.md— "Via Direct EventEmitter" section missing TypeScript and Rust tabs. The section contained only bare Python code with no tab wrapper. Added=== "Python"/=== "TypeScript"/=== "Rust"tabs consistent with every other behavioral section in the file.docs/features/observability.md— Lowercasemustin normative Requirements section.InMemoryExporter must be boundedcorrected toInMemoryExporter MUST be boundedper RFC 2119.PROTOCOL_SPEC.md §5.16— RFC 2119 keywords unbolded. AllMUST,MUST NOT, andSHOULDoccurrences in the §5.16 Pipeline Control Flow Requirements section (intro sentence + 5 numbered items) were plain text. Updated to**MUST**/**MUST NOT**/**SHOULD**consistent with the rest of the specification.conformance/README.md— Non-Standard Test Patterns section incomplete.schema_hardening_recursive.json(shared root-levelschema) andschema_hardening_formats.json(root-levelformat_mappingsreference metadata) were not documented alongside the existingcontext_serialization.jsonandannotations_extra_round_trip.jsonentries. Added entries explaining both patterns for SDK test runner authors.
- PROTOCOL_SPEC §5.7 —
trace_idformat pattern tightened fromformat: uuid(ambiguous: dashed vs hex) topattern: "^[0-9a-f]{32}$"(32-char lowercase hex, W3C Trace Context compatible). Resolves internal spec contradiction between §5.7 (format: uuid), §10.5 example (dashed UUID), and §10.5traceparent_header(hex without dashes). - PROTOCOL_SPEC §10.5 — Trace ID Format section rewritten to align with W3C Trace Context Level 2. Adds explicit
external_trace_parent_handlingrules: strict 32-hex validation only; all-zero and all-f rejected per W3C; no auto-normalization (dashed UUID stripping, case folding) atContext.create— pushed to TraceParent parser or user's ContextFactory. Implementations MUST regenerate + log WARN on invalid input; MUST NOT raise.
- PROTOCOL_SPEC §5.15.2 —
DEPENDENCY_VERSION_MISMATCHerror code (new, non-retryable). Raised when a module independencies.requiresexists but its registered version does not satisfy the declaredversionconstraint. Fordependencies.optionalthe same situation logs WARN and skips the dependency edge. conformance/fixtures/dependency_version_constraints.json— 15 cross-SDK test cases covering exact match,>=/<=, range (>=1.0.0,<2.0.0), caret (^1.2.3, including^0.2.3major-zero semantics), tilde (~1.2.3), partial-version shortcuts ("1"matches1.x.x), no-constraint accepts any, and optional-dependency skip-on-mismatch.- PROTOCOL_SPEC §5.7 — Note on external correlation IDs. Documents that existing projects' request/correlation identifiers (
X-Request-ID, ULID, AWS X-Ray, etc.) SHOULD be preserved incontext.data["x-correlation-id"]alongsidetrace_id, not used to overwrite it. Establishes the dual-ID model at the spec level. - PROTOCOL_SPEC §10.5 — Forward-compatibility Note. Non-normative acknowledgment that
trace_idformat is a versioned contract, not a permanent guarantee. Future protocol versions MAY introduce alternative formats or structured trace IDs for multi-agent or non-linear trace topologies. docs/guides/integrating-existing-projects.md— New user guide covering Django, Express, and Actix integration patterns for projects already carrying their own request-ID / correlation-ID system.conformance/fixtures/context_trace_parent.json— Cross-language fixture with 10 test cases covering valid 32-hex, dashed UUID rejection, uppercase rejection, W3C-invalid all-zero/all-f rejection, length errors, non-hex characters, empty string, and the no-trace_parent baseline.docs/spec/DECLARATIVE_CONFIG_SPEC.mdv1.0 — New unified specification for declarative YAML configuration across all three SDKs. Covers bindings YAML (§3), pipeline config (§4), entry-point meta (§5),auto_schemasemantics (§6), canonical error model with exact message templates (§7), configurable policy limits (§9). Establishes core principles: YAML syntax 100% consistent across SDKs; never silently drop fields; auto-processing as default; JSON Schema for structure, apcore.yaml for policy.schemas/binding.schema.jsonupdates — Relaxedtargetregex to support TypeScript ESM specifiers (./relative,@scope/pkg) and Rust handler-map keys.auto_schemafield now accepts boolean or"true"/"permissive"/"strict"enum. Schema mode mutex rules rewritten fromoneOftoallOf+if/then(supports implicit auto default when no mode specified).DisplayOverlaychanged fromadditionalProperties: falsetopropertyNamespattern +additionalProperties: SurfaceOverridefor future surface extensibility.Annotationsexpanded from 5 to 12 fields (addedstreaming,cacheable,cache_ttl,cache_key_fields,paginated,pagination_style,extra) to align withmodule-meta.schema.json.versionfield pattern moved to configurable policy (version_require_semver). Addedspec_versiontop-level field.schemas/apcore-config.schema.json—pipelineandvalidationsections — NewPipelineConfigdefinition withremove,configure,stepsstructure;PipelineStepwithtype/handlermutual exclusion,after/beforepositioning, and metadata fields (match_modules,ignore_errors,pure,timeout_ms). NewValidationConfigwith configurable policy limits for bindings (description_max_length,documentation_max_length,tags_pattern,version_require_semver) and pipeline (step_name_max_length,timeout_ms_max).conformance/fixtures/binding_yaml_canonical.yaml— Cross-SDK binding YAML conformance fixture with 3 entries testing explicit auto_schema (permissive), explicit input/output schemas with display overlay, and auto_schema strict mode. All three SDKs must parse this fixture identically.conformance/fixtures/binding_errors.json— 6 canonical error message test cases for cross-SDK byte-for-byte message parity (BindingFileInvalidError,BindingSchemaModeConflictError,BindingSchemaInferenceFailedError,PipelineHandlerNotSupportedError,BindingInvalidTargetError,BindingModuleNotFoundError).
- PROTOCOL_SPEC §8 Error Code Registry — Added missing
DEPENDENCY_VERSION_MISMATCHentry alongsideDEPENDENCY_NOT_FOUND. Previously the behavior was implied by §5.3 version constraint syntax (^,~, ranges) without a corresponding error code in §8, leaving SDKs without a normative failure mode. - SDK compliance with §5.15.2
DEPENDENCY_NOT_FOUND— All three SDKs (apcore-python,apcore-typescript,apcore-rust) now raise the spec-mandatedDEPENDENCY_NOT_FOUNDerror code for missing required dependencies. Previously all three raisedMODULE_LOAD_ERROR, silently diverging from the spec. - SDK
CircularDependencyError.details.cycle_pathparity — Rust SDK now carriescycle_pathas structured details, matching Python (details["cycle_path"]) and TypeScript (details.cyclePath). Previously Rust only placed the path in the message string, forcing downstream consumers to parse it. - PROTOCOL_SPEC §8.5 —
trace_idexample updated to 32-char lowercase hex. The error response example still used the old dashed UUID form (550e8400-e29b-41d4-a716-446655440000), contradicting the §5.7 and §10.5 tightening shipped in this same release. Updated to4bf92f3577b34da6a3ce929d0e0e4736.
- The pre-existing
MUST NOT allow externally provided unvalidated trace_idrule in §10.5 is superseded by the explicit validation/normalization pipeline — "unvalidated" is now impossible because every input is either accepted verbatim or replaced with a fresh trace_id.
Breaking changes in this release. See
MIGRATION-v0.18.mdfor the consolidated migration guide covering all four repositories.
APCoreconstructor simplified — removedconfig_pathparameter; useConfig.load()instead (e.g.APCore(config=Config.load("apcore.yaml"))). This applies to all SDKs: Python, TypeScript, and Rust. The explicit two-step pattern keeps the constructor focused and avoids mutual-exclusivity validation.- System module JSON Schemas — 6 new output schemas in
schemas/:sys-control-reload-module,sys-control-toggle-feature,sys-control-update-config,sys-health-module,sys-health-summary,sys-manifest-full. Each declares$schema,$id,additionalProperties: false, and descriptions on every property. - Conformance README coverage gap table — Documents 6 algorithms (A01, A07, A12, A21, A22, A23) that lack conformance fixtures.
- Conformance README non-standard patterns section — Documents
sub_casesandforbidden_root_keystest patterns for SDK implementers. - PROTOCOL_SPEC §2.7 —
canonical_idmaximum length raised from 128 to 192 characters. Motivated by deep-namespace languages (Java/.NET/Spring FQN-derived IDs) where snake_case-converted fully-qualified names can exceed 128 in edge cases. 192 is filesystem-safe (192 + ".binding.yaml" = 205 bytes < 255-byte filename limit on ext4/xfs/NTFS/APFS/btrfs) and remains withinVARCHAR(255)for typical persistence layers. MCP alias 64-char hard limit (OpenAI function name spec) is unchanged and still requires alias mapping for any module_id > 64. Schemas updated:binding.schema.json,module-schema.schema.json,module-meta.schema.jsondeclaremodule_id.maxLength: 192;acl-config.schema.jsoncallers/targetspattern strings raised to 192 to remain symmetric. Algorithm A01 (directory_to_canonical_id) Step 6 length threshold updated to 192. Conformance test T01-006 boundary updated to>192 chars. Forward-compatible relaxation: producers targeting mixed-version ecosystems should keep IDs ≤ 128 until all consumers are upgraded. - PROTOCOL_SPEC §5.6 yaml block now declares
required_attributes:(input_schema,output_schema,description) explicitly. The pseudocode block already marked these as "Required definitions" but the yaml block beneath only listedrequired_methodsandoptional_attributes, so a reader of the yaml alone could not see the attribute requirement. Closes a sync audit contradiction. - PROTOCOL_SPEC §4.4.1 — Annotations Extension Field (
extra) Wire Format — New normative section defining the canonical on-the-wire shape ofModuleAnnotations.extra. Producers MUST serialize as a nested{"extra": {...}}object and MUST NOT flatten extension keys to the annotations root. Consumers MUST accept the nested form; legacy top-level overflow keys MAY be tolerated for one MINOR cycle. When both forms appear in the same input, the nested value wins. extrafield inAnnotationsschema —schemas/module-meta.schema.jsonnow declaresextraas an object withadditionalProperties: true. The outerAnnotationsobject retainsadditionalProperties: false, so unknown root-level keys are no longer silently accepted at the schema layer.conformance/fixtures/annotations_extra_round_trip.json— 8 cross-language test cases locking the wire format: canonical nested round-trip, empty extra, namespaced keys, Unicode and nested object values, legacy flattened deserialization tolerance, nested-wins precedence, forbidden-root-keys negative case, and dotted-keys-are-not-paths.MIGRATION-v0.18.md— Consolidated migration guide covering all four breaking changes shipped in this release (annotations wire format, apcore-rust Config restructure, apcore-python event alias removal, misc cleanup).- 8 new feature specification docs. The following features were implemented in both
apcore-pythonandapcore-typescriptSDKs but had no corresponding feature spec in the protocol repo:docs/features/error-system.md— Structured error hierarchy (30+ error types), error codes, AI guidance fields (retryable,ai_guidance,user_fixable,suggestion),ErrorCodeRegistryfor custom module error codes.docs/features/extension-system.md—ExtensionManagerwith 6 built-in extension points (discoverer,middleware,acl,span_exporter,module_validator,approval_handler), plugin wiring viaapply().docs/features/call-chain-guard.md— Algorithm A20: call depth limiting, circular call detection, frequency throttling with configurable thresholds.docs/features/cancellation.md—CancelTokencooperative cancellation, executor timeout integration with 5-second grace period.docs/features/async-tasks.md—AsyncTaskManagerfor background module execution with concurrency semaphore, task lifecycle tracking, and cleanup.docs/features/streaming.md— Three-phase streaming pipeline (setup → chunk emission → post-validation), deep merge with depth cap.docs/features/identity-system.md—Identitydata structure with well-known types (user,service,ai,system,anonymous),ContextFactoryprotocol for web framework integration.docs/features/apcore-client.md—APCoreunified client feature spec covering initialization modes, auto-registration behavior, and method summary.
mkdocs.ymlnavigation updated. Feature Specifications section expanded from 11 to 19 entries (alphabetically sorted).README.mdDocumentation Index updated. Added all 8 new feature docs to the Feature Specifications table.
- PROTOCOL_SPEC §10.8 — Span attribute
"caller"corrected to"caller_id". The span naming convention section listed"caller"as a SHOULD attribute, inconsistent with thecaller_idfield name used everywhere else in the specification (§2.1, §6, §7, Context object). All SDKs already usedcaller_idin their span implementations. - PROTOCOL_SPEC §9.3 — RFC 2119 keyword compliance. Five constraint validation statements in the
A12_validate_configpseudocode used lowercasemustinstead of uppercaseMUST. Corrected to match RFC 2119 normative intent. mkdocs.ymlnavigation — 4 orphaned docs now reachable. Addedapi/client-api.md,features/config-bus.md,features/event-system.md, andfeatures/system-modules.mdto the site navigation. These pages existed and were referenced in README.md but were not in the MkDocs nav tree.- JSON Schema property descriptions. Added missing
descriptionfields to 4 properties inmodule-schema.schema.json(ErrorSchemaObject.type,.properties,.required, and thedescriptionproperty definition) and 3 surface-override objects inbinding.schema.json(cli,mcp,a2a). All properties now comply with the "every property must have a description" rule. docs/features/streaming.md— Step count corrected from "Steps 1–6" to "Steps 1–7". The text listed 7 pipeline phases (Context Creation through Input Validation) but labelled them "Steps 1–6". PROTOCOL_SPEC §6574 explicitly states "Steps 1–7 identical to call()" forstream().docs/api/executor-api.md—validate()step count corrected. Previously said "Steps 1-7"; now says "Steps 1-6, plus optional module-level preflight" to match PROTOCOL_SPEC §12.8.docs/spec/conformance.mdT11-001 — Deprecatedexecute_async()replaced withcall_async(). Theexecute_async()method was removed in v0.10; conformance table still referenced the old name.docs/guides/creating-modules.md— Rust examples added. Two tabbed code sections (module definition and module usage) only had Python and TypeScript examples. Added Rust tabs with complete, importable examples usingapcore::{Module, Context, Registry, Executor}.- Cross-language
extraserialization divergence — Audit revealed thatapcore-rust≤ 0.17.1 used#[serde(flatten)]and emitted extension keys at the annotations root, whileapcore-pythonandapcore-typescriptemitted nestedextraobjects. A binding round-tripped through Rust would silently lose theextrapayload (the nested object collapsed intoextra["extra"]). All three SDKs are now aligned on the nested form per §4.4.1. - Python/TypeScript precedence inversion — Both SDKs previously merged top-level overflow over explicit nested
extra({**explicit, **overflow}), making nested values losable. Per §4.4.1 rule 7, nested now wins. Behavior change is observable only when the same key appears in both forms in the same input — a pathological case that no conformant producer emits. apcore-rust Configno longer silently ignored spec-conformant YAML. The struct previously declared executor and observability fields at the root ofConfiginstead of nested underexecutorandobservabilitynamespaces, contradicting PROTOCOL_SPEC §9.1 and the Python/TypeScript SDKs. Loading a YAML file that used the canonical nested form would cause typed fields to remain at default values while the user data ended up in an unusedsettingsHashMap entry. v0.18.0 restructuresapcore-rust Configto a nested form, drops the legacy short field names, and rejects v0.17.x-style YAML with a hard error pointing atMIGRATION-v0.18.md. Seeapcore-rust/CHANGELOG.mdfor the full type-by-type rename table.docs/spec/design-context-annotations-acl.mdno longer contradicts shipped spec. The historical design document still claimed "ModuleAnnotations is frozen with 11 fields" and recommended#[serde(flatten)]for the Rust extra field. A superseded banner now points readers at PROTOCOL_SPEC §4.4.1 for current normative behavior; the original text is preserved for historical context.mkdocs.ymlHome nav now points atREADME.md(which exists) instead ofindex.md(which never did), eliminating a noisy mkdocs build warning.- PROTOCOL_SPEC §2.1 — Algorithm A01 step 6 and the directory_to_id YAML format block still said
max_length: 128after the §2.7 bump to 192. Both updated to 192. Internal contradiction with the §2.7 EBNF constraint and the version history changelog entry resolved. - PROTOCOL_SPEC §3868 cross-reference cleanup —
Self-Evolution: ... runtime reconfiguration (see §6.6, §10)repointed to(see §9.11 Hot-Reload, §10 Observability). §6.6 is "System Module Permissions" — unrelated to runtime reconfiguration. Stale reference from a §6.x renumbering. docs/spec/design-execution-pipeline.md:1248— Phase 4 implementation table replaced obsoleteVALIDATE_ONLYpreset with the canonicalMINIMALpreset, matching the canonical 5-preset enumeration at line 680 (standard, internal, testing, performance, minimal). VALIDATE_ONLY was replaced bydry_runper §4.docs/spec/design-execution-pipeline.md:1192—validate()return type table updated to show all three SDKs unified onPreflightResult(Rust column previously showedValidationResultwith a "long-term plan to unify" note; the unification was completed in this release).docs/features/approval-system.md— Wrong pipeline step numbers. Input Validation was cited as "Step 6" in three places; it is actually Step 7. Middleware Before Chain is Step 6. Corrected all references.docs/features/event-system.md— Missing canonical event name prefix.error_threshold_exceededandlatency_threshold_exceededwere listed without theapcore.prefix. Per PROTOCOL_SPEC §9.16, canonical names areapcore.error.threshold_exceededandapcore.latency.threshold_exceeded; the unprefixed forms are legacy aliases. Event Types table corrected.docs/features/config-bus.md— Wrong PROTOCOL_SPEC section references. "Typed Bind (§9.8)" corrected to §9.9.3 (§9.8 is Environment Variable Override). "Hot Reload (§9.9)" corrected to §9.11 (§9.9 is Namespace-Aware Access API).docs/features/streaming.md— TypeScript example fixed. Replaced incorrectclient.module()withasync function*(FunctionModule doesn't support streaming) with correctModuleinterface implementation using astream()method. Fixed deep merge table: arrays are replaced, not concatenated.docs/features/error-system.md— Reserved prefix list corrected. Added missing prefixes (DEPENDENCY_,CALL_,MIDDLEWARE_,VERSION_,ERROR_CODE_), removed incorrect ones (RELOAD_,EXECUTION_,ERROR_FORMATTER_). Added missing error classes (MiddlewareChainError,ErrorCodeCollisionError,DependencyNotFoundError).docs/features/call-chain-guard.md— Algorithm description corrected. Circular detection now accurately describes the prior-chain extraction + last-occurrence check, matching both SDK implementations.docs/features/cancellation.md— TypeScript Context example fixed.CancelTokenis passed vianew Context()constructor, notContext.create()which doesn't accept it.docs/features/async-tasks.md— Type corrections.TaskInfo.resulttype corrected fromdict[str, Any]toAny.get_result()return type corrected fromdicttoAny.docs/concepts.md— RemovedModuleinheritance. Allclass XModule(Module):examples changed toclass XModule:to match the spec requirement "Modules MUST NOT inherit from an ABC."docs/architecture.md— Fixed 3 broken links.../api/*.mdpaths corrected to./api/*.md(architecture.md is in docs/, not a subdirectory).docs/guides/middleware.md— Removed non-existentAsyncMiddlewareclass. Replaced with standardMiddlewaresubclass with async method overrides, which both SDKs support.docs/api/executor-api.md— Sync/async clarity.call()docstring updated from "Synchronously call module" to "Call module (synchronous in Python, async in TypeScript/Rust)."docs/api/client-api.md— Sync/async clarity. Section "4.1 Synchronous Call" renamed to "4.1 Basic Call" with note explaining Python sync vs TypeScript/Rust async.docs/spec/type-mapping.md— Go/Java SDK scope clarified. Added note that Go and Java type mappings are for future implementers; only Python, TypeScript, and Rust have official SDKs.docs/guides/— Cross-language notes added.acl-configuration.md,adapter-development.md, andtesting-modules.mdnow have a note at the top explaining how Python examples apply to TypeScript and Rust.- PROTOCOL_SPEC.md RFC 2119 compliance — 5 instances of lowercase bold
**must**/**should**in normative contexts (§1.6, §4.2, §5.6, §11.6) corrected to uppercase**MUST**/**SHOULD**. - PROTOCOL_SPEC.md §11.6 extension point semantics — Clarified ambiguous mixed requirement: "Implementations SHOULD support the following extension points. Each supported extension point MUST define a clear interface contract."
docs/spec/algorithms.mdRFC 2119 compliance — 5 lowercasemustin A12 config validation constraints (lines 895–899) corrected to uppercaseMUST.schemas/sys-control-update-config.schema.json— Added missingtypedeclarations toold_valueandnew_valueproperties.schemas/defaults.schema.json— Added explicitadditionalProperties: falseto root and all 9 nested objects.schemas/sys-health-summary.schema.json— Added explicitadditionalProperties: falseto root and all 3 nested objects.- CHANGELOG.md v0.16.0 date — Corrected from 2026-04-05 to 2026-04-03 (matching git history).
docs/features/config-bus.md— Converted all code examples to cross-language tabbed format (=== "Python"/=== "TypeScript"/=== "Rust"). Added missing Rust tab in Introspection section.- Orphaned docs removed from
docs/features/— Moved 4 code-forge planning docs (acl-conditions-redesign.md,annotations-redesign.md,context-redesign.md,overview.md) out of the published docs tree; these already exist inplanning/. docs/spec/design-execution-pipeline.md— Mergeddesign-pipeline-v2.mdcontent, removed stale legacy alias column.- Event naming standardization — Canonical
apcore.*prefix enforced across event system docs and identity rule schemas. - Multi-language documentation — Added cross-language examples and tabbed sections to feature docs, API references, and getting-started guide.
- Legacy event aliases.
apcore-pythonno longer emitsmodule_health_changedorconfig_changedalongside the canonicalapcore.module.toggled,apcore.health.recovered,apcore.config.updated, andapcore.module.reloadedevents. The dual-emission deadline was originally v0.16.0 but was missed; this release completes the cleanup. See migration guide §3.
- Legacy flat
Configfield names.Config.max_call_depth,Config.default_timeout_ms,Config.global_timeout_ms,Config.max_module_repeat,Config.enable_tracing,Config.enable_metricsare gone. Use the nested namespaces (Config.executor.*,Config.observability.*). The string-key API also drops the legacy bare-name aliases —config.get("max_call_depth")now returnsNone; useconfig.get("executor.max_call_depth"). See migration guide §2.
minimalstrategy preset — 4-step pipeline (context_creation→module_lookup→execute→return_result) for pre-validated internal hot paths. Documented in all three SDKs and spec.- Core vs Optional steps overview — New §4.0 in
design-execution-pipeline.mdwith ASCII diagram showing which 4 steps are mandatory and which 7 are removable. - Middleware vs Custom Step selection guide — Decision matrix in
design-execution-pipeline.md§4.2 and practical guide with cross-language examples indocs/guides/middleware.md§11. requires/providesstep dependency metadata — Optional advisory fields onBaseStep(Python),Stepinterface (TypeScript), andSteptrait (Rust).ExecutionStrategywarns at construction and insertion time if a step'srequiresare not satisfied by preceding steps'provides.- Execute step replacement warning —
!!! warningadmonition indesign-execution-pipeline.mdexplaining risks of replacing theexecutestep. - Strategy summary table — All preset strategies documented with step counts, removed steps, and use cases.
- PROTOCOL_SPEC.md pipeline order — Steps 6/7 swapped to match all SDK implementations (
middleware_before→input_validation). Step 2 renamed from "Safety Checks" to "Call Chain Guard". - design-execution-pipeline.md alignment — Step inventory table, code examples, JSON pipeline example, and strategy references updated for correct stage order, naming, and
validate_onlyremoval. validate_onlystrategy removed from spec — Never implemented in any SDK;validate()method withdry_run=Trueprovides the same behavior more cleanly.
- 4 new BaseStep fields —
match_modules(glob patterns for selective step execution),ignore_errors(fault-tolerant continuation on step failure),pure(no side effects, safe for dry-run/validate mode),timeout_ms(per-step timeout in milliseconds). Implemented across all three SDKs (Python, TypeScript, Rust). PipelineContext.dry_run— Whentrue, PipelineEngine skips steps withpure=false, enablingvalidate()to run user-defined pure steps automatically.PipelineContext.version_hint— Passed through to module_lookup for version negotiation.PipelineContext.executed_middlewares— Tracks which middleware ran for on_error recovery chain.StepTrace.skip_reason— Records why a step was skipped:"no_match","dry_run", or"error_ignored".- YAML pipeline configuration (Python) —
pipelinesection inapcore.yamlwithremove,configure, andstepsdirectives. Step resolution viatype(registry) andhandler(import path).
safety_check→call_chain_guard— Renamed across all SDKs to accurately describe the step's purpose (call chain depth/cycle/repeat checks, not transport-level rate limiting).- TypeScript
builtin.prefix removed — All built-in step names in the TypeScript SDK dropped thebuiltin.prefix for cross-SDK consistency (e.g.,builtin.context_creation→context_creation).
- Steps 6 and 7 swapped —
middleware_beforenow executes beforeinput_validation(was the reverse). Middleware transforms are now validated by the subsequent input_validation step, aligning with the Kubernetes Mutating → Validating admission order.
- Middleware transforms now validated — Previously, middleware modifications to inputs were either never re-validated (production code) or silently discarded (pipeline abstraction). The step order swap ensures transformed inputs pass schema validation.
env_styleparameter — Three modes for environment variable key conversion:auto(default, matches againstdefaultstree),nested(single_→.),flat(no conversion). Resolves flat snake_case config key conflicts.max_depthparameter — Limits nesting depth for env var key conversion (default: 5). Prevents excessively deep nesting from long env var names.env_prefixauto-derivation — Whenenv_prefixis not provided, auto-derived from namespace name vianame.upper().replace("-", "_").env_mapparameter — Explicit mapping of bare (unprefixed) env var names to config keys within a namespace (e.g.,{"REDIS_URL": "cache_url"}).Config.env_map()class method — Global bare env var → top-level config key mapping (e.g.,{"PORT": "port"}).CONFIG_ENV_MAP_CONFLICTerror — Raised when the same env var is claimed by multiple env_map registrations.
ContextKey<T>typed accessor — Generic type-safe wrapper forcontext.dataaccess withget(),set(),delete(),exists(),scoped()methods. Available in Python, TypeScript, and Rust.- Built-in context key constants —
TRACING_SPANS,TRACING_SAMPLED,METRICS_STARTS,LOGGING_START,REDACTED_OUTPUT,RETRY_COUNT_BASEexported for middleware authors. _context_versionserialization — Context serialization now includes_context_version: 1for forward compatibility. Deserialization warns on unknown versions but proceeds.- Context
serialize()/deserialize()methods — Explicit serialization API with data key filtering (underscore-prefixed keys excluded).
extrafield onModuleAnnotations— Free-form extension dictionary for ecosystem packages and user metadata (e.g.,extra={"mcp.category": "tools"}).pagination_styletype relaxed — Changed fromLiteral["cursor", "offset", "page"]to openstring, allowing custom pagination strategies.DEFAULT_ANNOTATIONSconstant — Exported frozen default annotations instance.from_dict()classmethod (Python) — Deserializes annotations with unknown keys captured inextra.createAnnotations()factory (TypeScript) — Convenience factory accepting partial overrides.- Canonical snake_case wire format (TypeScript) —
annotationsToJSON()/annotationsFromJSON()for cross-language serialization.
ACLConditionHandlerprotocol — Extensible condition evaluation interface. Python: sync + async protocols. TypeScript:boolean | Promise<boolean>. Rust:#[async_trait].ACL.register_condition()class method — Register custom condition handlers (e.g.,ip_range,time_window).$orand$notcompound operators — Built-in compound condition handlers for OR and NOT logic in ACL rules.async_check()method — Async ACL check alongside existing synccheck(), supporting async condition handlers.- Fail-closed for unknown conditions — Unknown condition keys now log a warning and return False (deny), instead of being silently ignored.
Stepprotocol / interface / trait — Pluggable pipeline step withname,description,removable,replaceable, and asyncexecute().ExecutionStrategyclass — Ordered list of steps withinsert_after(),insert_before(),remove(),replace()modification API.PipelineEngine— Executes strategy steps with index-based loop, skip_to support, trace accumulation, and abort handling.PipelineTrace/StepTrace— Complete execution trace for AI introspection and learning.- 11 built-in steps —
BuiltinContextCreation,BuiltinSafetyCheck,BuiltinModuleLookup,BuiltinACLCheck,BuiltinApprovalGate,BuiltinInputValidation,BuiltinMiddlewareBefore,BuiltinExecute,BuiltinOutputValidation,BuiltinMiddlewareAfter,BuiltinReturnResult. - Preset strategies —
build_standard_strategy()(11 steps),build_internal_strategy()(skip ACL/approval),build_testing_strategy()(minimal),build_performance_strategy()(skip middleware). Executor.strategyparameter — Optional, backward-compatible. When omitted, uses standard 11-step pipeline.call_with_trace()/call_async_with_trace()— Returns(result, PipelineTrace)tuple for observability.register_strategy()/list_strategies()/describe_pipeline()— Strategy introspection API.
- Data key naming convention — Internal middleware keys migrated from legacy names (
_metrics_starts,_usage_starts,_obs_logging_starts) to_apcore.mw.*convention. All middleware now uses typedContextKeyconstants.
- Rust
ApprovalRequestspec alignment — Added requiredcontextfield (Option<Context<Value>>) and changedannotationstype fromHashMaptoModuleAnnotationsper spec §7.3.1. - Rust
DependencyInfofield rename — Renamednametomodule_idfor cross-SDK consistency with Python/TypeScript. - Rust config env fallback — Fixed namespace-mode
APCORE_*env var fallback to resolve to top-level paths instead of incorrectly prependingapcore.prefix. - Rust
config_envconformance test — Added missing conformance test (was 9/10, now 10/10 fixtures). - Rust Context field alignment — Removed non-spec fields (
created_at,parent_trace_id,trace_context). Changedglobal_deadlinefromOption<Instant>toOption<f64>(epoch seconds). - Rust Identity immutability — Fields made private with pub getters. Serde compatibility via
IdentityRawpattern. - TypeScript
globalDeadlinefield — AddedglobalDeadline: number | nullto Context (was missing). - Rust system.control module — Extracted into dedicated
control.rsfile (was inline inmod.rs). - TypeScript
removeRulecomparison — Fixed to use element-wise array comparison instead ofJSON.stringify. - Rust empty callers matching — Empty callers list now matches none (aligned with Python/TypeScript).
- API documentation audit (13 fixes) — Corrected Executor constructor (missing
strategyparam),cache_key_fieldstype (tuple not list),ModuleAnnotations.extrafield, Reserved Words,extensions_dirdefault,ModuleExampledefaults, Contextserialize()/deserialize(),_global_deadline,preflight()/describe()methods,PreflightCheckResult.warnings,$or/$notACL examples. - Cross-language tabbed examples — Added TypeScript tab to system-modules.md, converted client-api.md to 3-language tabs, fixed bare code blocks in 4 API docs.
- Env prefix convention simplified — Removed the
^APCORE_[A-Z0-9]reservation rule from namespace registration. Sub-packages now use single-underscore prefixes (APCORE_MCP,APCORE_OBSERVABILITY,APCORE_SYS) instead of the double-underscore form. The longest-prefix-match dispatch algorithm already disambiguates correctly; the previous restriction was unnecessary. - Built-in namespace env prefixes:
APCORE__OBSERVABILITY→APCORE_OBSERVABILITY,APCORE__SYS→APCORE_SYS.
- Config Bus Architecture (§9.4) — apcore.Config upgraded from internal configuration tool to ecosystem-level Config Bus. Any package (apcore ecosystem or third-party) can register a namespace with optional JSON Schema validation, environment variable prefix, and defaults. Design principles: bus not center, zero-cost adoption, gradual integration, cross-language consistency, strict/flexible coexistence
- Namespace Registration (§9.5) —
Config.register_namespace(name, schema, env_prefix, defaults)API with cross-language examples (Python, TypeScript, Rust, Go, Java). Global (class-level) registry shared across Config instances. Late registration permitted with explicitreload()to apply. Nounregister_namespacein this version - Unified Configuration File (§9.6) — Single YAML file with namespace-partitioned sections. Automatic mode detection: legacy mode (no
apcore:key, fully backward compatible) vs namespace mode (apcore:key present)._configreserved namespace for meta-configuration (strict,allow_unknown) - Mount Mechanism (§9.7) —
config.mount(namespace, from_file|from_dict)for attaching external configuration sources without requiring a unified file. Primary integration path for third-party projects with existing config systems - Per-Namespace Env Override (§9.8) — Each namespace declares its own
env_prefix. Longest-prefix-match dispatch algorithm resolves ambiguity.APCORE_MCPdouble-underscore convention for apcore sub-packages to avoid collision withAPCORE_prefix. Compatibility note for apflow's simpler env convention - Namespace-Aware Access API (§9.9) —
config.get("namespace.key.path")with dot-path namespace resolution algorithm.config.namespace(name)for full subtree retrieval.config.bind(ns, type)/config.get_typed(path, type)for typed access.Config.registered_namespaces()for introspection - Validation Algorithm A12-NS (§9.10) — Extended A12 for namespace mode: validates
apcorenamespace with original algorithm, validates registered namespaces against their JSON Schema, handles unknown namespaces per strict/allow_unknown settings - Hot-Reload Namespace Support (§9.11) —
config.reload()re-reads YAML, re-detects mode, re-applies namespace defaults and env overrides, re-validates, and re-reads mounted files - Cross-Language Implementation Requirements (§9.12) — MUST/SHOULD API surface table, language-idiomatic naming matrix, thread safety requirements, parameter passing style note
- Ecosystem Integration Patterns (§9.13) — Convention table for all apcore packages (namespace, env prefix, schema file). Third-party defensive integration pattern. Framework auto-registration examples (Django AppConfig.ready(), FastAPI module-level, NestJS)
- Config Discovery (§9.14) — Optional (
MAY) automatic config file discovery with search order:$APCORE_CONFIG_FILE→./project.yaml→./apcore.yaml→ user-level config - New error codes —
CONFIG_NAMESPACE_DUPLICATE,CONFIG_NAMESPACE_RESERVED,CONFIG_ENV_PREFIX_CONFLICT,CONFIG_MOUNT_ERROR,CONFIG_BIND_ERROR
Three mechanisms addressing ecosystem consistency across apcore, apcore-mcp, apcore-cli, apcore-a2a and third-party packages:
-
Error Formatter Registry (§8.8) — Shared
ErrorFormatterprotocol and registration point. apcore-mcp and apcore-a2a each independently implement protocol-specific error mappers (MCP camelCase/sanitization, A2A JSON-RPC code mapping); this registry makes the contract explicit and discoverable. Adoption is SHOULD-level for ecosystem adapters — apcore does not ship adapter-specific formatters. New error code:ERROR_FORMATTER_DUPLICATE -
apcore Built-in Namespace Registrations (§9.15) — The framework pre-registers two namespaces for its own subsystems, applying the Config Bus pattern to apcore's own internal configuration. Both promote existing flat keys already present in apcore-python's
config.py; migration is 1:1 with no breaking changes:observability(APCORE_OBSERVABILITY) — Extractsapcore.observability.*flat keys (tracing, metrics, logging, error_history, platform_notify) into a dedicated namespace. Adapter packages (apcore-mcp, apcore-a2a, apcore-cli) should read from this namespace rather than using independent logging defaultssys_modules(APCORE_SYS) — Promotesapcore.sys_modules.*flat keys into a dedicated namespace.register_sys_modules()prefersconfig.namespace("sys_modules")in namespace mode withconfig.get("sys_modules.*")legacy fallback
-
Event Type Naming and Collision Fix (§9.16) — Resolves two confirmed collisions in apcore-python's emitted event types:
"module_health_changed"was used for two distinct events (toggle on/off vs. error rate recovery); replaced by canonical namesapcore.module.toggledandapcore.health.recovered"config_changed"was used for two distinct events (key update vs. module reload); replaced byapcore.config.updatedandapcore.module.reloaded- Establishes dot-namespaced naming convention:
apcore.*reserved for core,apcore-mcp.*/apcore-a2a.*/apcore-cli.*for adapters - All four legacy short-form names remain emitted as aliases during transition
docs/features/event-system.md— Fixed severity levels fromwarning/criticaltowarn/fatal, aligning with PROTOCOL_SPEC §10.2docs/features/core-executor.md— Fixed Identity description: removed incorrectpermissionsfield, corrected toid,type,roles,attrsper PROTOCOL_SPEC §5.7docs/features/core-executor.md— Added missingApprovalPendingErrorto Approval Gate description per PROTOCOL_SPEC §7.4docs/features/middleware-system.md— Addedretry.pyto Key Files table (was described in Components but missing from file list)docs/features/observability.md— Added concrete metric names (apcore_module_calls_total,apcore_module_errors_total,apcore_module_duration_seconds) to convenience method documentation
- Fixed duplicate
### 10.5section numbering — renumbered Sensitive Data Redaction to §10.6, Sampling Strategy to §10.7, Span Naming Convention to §10.8 - Fixed
### 8.1.1misnumbered under §9.1 — corrected to §9.1.1 - Fixed
### 10.8.xmisnumbered under §11.8 — corrected to §11.8.1–§11.8.4 - Fixed middleware priority model contradiction between §11.2 (explicit 0-1000) and §12 (registration order) — §12 now aligns with §11.2
- Fixed
on_errorexamples in README.md and concepts.md — changedraise errorto correct return-based contract (return None/return dict) - Updated "Last Updated" date to 2026-03-24
- Added Approval System (§7) and Event System to "Core Protocol Includes" list in SCOPE.md
- Added AI-Perceivable brand definition block to README header
- Added "Perceived → Understood → Executed" progression table to "Why AI-Perceivable?" section
§5.13 Display Overlay (specified in v0.13.0) is now implemented across the official adapter stack:
| Package | Version | What was implemented |
|---|---|---|
apcore-toolkit |
0.4.0 | DisplayResolver — §5.13 resolve priority chain, MCP alias sanitization/64-char limit, CLI alias validation, suggested_alias fallback, binding_path file/directory loading |
apcore-cli |
0.3.0 | CLI command routing from metadata["display"]["cli"]["alias"]; descriptor cache; JSON output reads display overlay |
apcore-mcp |
0.11.0 | MCP tool name and description from metadata["display"]["mcp"]; guidance appended to tool description |
apcore-a2a |
0.3.0 | A2A skill id/description/tags from metadata["display"]["a2a"]; removed dead _build_extensions() |
fastapi-apcore |
0.4.0 | binding_path parameter on create_cli() / create_mcp_server(); DeprecationWarning for simplify_ids=True |
- §5.14 Convention Module Discovery — new optional protocol capability for zero-decorator module registration via
commands/directory convention. Supports cross-language function discovery with schema inference from type annotations.
- Rebrand: aipartnerup → aiperceivable
- Caching annotations (§4.4) —
cacheable,cache_ttl,cache_key_fieldsannotation fields for AI-aware caching decisions - Pagination annotations (§4.4) —
paginated,pagination_style(cursor/offset/page) for paginated result handling - AI Metadata Conventions (§4.6) — 13 standardized
x-metadata keys across 4 categories: Intent, Planning, Performance/Cost, Trust/Verification sunset_date(§5.2) — ISO 8601 date field for module deprecation lifecycleon_suspend()/on_resume()(§5.6, §12.7.3) — Optional lifecycle hooks for state preservation during hot-reload- Hot Reload with State Migration (§12.7.3) — New section with algorithm, constraints, and Python example
- Ecosystem documentation — Added apcore-mcp, apcore-a2a, apcore-cli, and apcore-testing to README and SCOPE
module-meta.schema.json— Addedstreaming,cacheable,cache_ttl,cache_key_fields,paginated,pagination_style,sunset_datedefinitions
- Rebranded from "universal module development framework" to "AI-Perceivable module standard" with three-tier messaging (slogan/subtitle/full definition)
- SCOPE.md — Expanded boundary decisions from 17 to 26 rows; updated requirements wording from "The framework" to "Implementations"
- Section renumbering — §12.7.3–12.7.8 renumbered after hot-reload insertion
- Lifecycle table (§12.7.1) — Reordered to show
on_resumeafteron_load, with note clarifying old/new instance distinction
- Cross-references — Fixed 10+ stale §11.7.x and §12.7.x references across architecture, registry-api, algorithms, and context-object docs
- Metadata key consistency — Aligned
x-max-latency-msdescription between README and PROTOCOL_SPEC
- Error catalog expanded (§8.2) — Added
MODULE_DISABLED,EXECUTION_CANCELLED,RELOAD_FAILEDerror codes with retryability classification and error hierarchy entries - UsageCollector formalized (§10.4) — Added usage tracking specification for
UsageCollectorandUsageMiddlewarebackingsystem.usage.*modules
- Shared conformance fixtures (
conformance/fixtures/) — 7 JSON fixture files for cross-language testing:pattern_matching,specificity,normalize_id,call_chain,error_codes,version_negotiation,acl_evaluation
- Context.child() naming (§12.7.2) — Standardized
derive()→child()to match SDK implementations - Forward-declared errors resolved —
GENERAL_NOT_IMPLEMENTEDandDEPENDENCY_NOT_FOUNDnow fully implemented in both SDKs
- Cross-reference links — Added links between API docs and feature docs (executor-api.md, context-object.md)
- Conformance known deviations — Updated status of error code implementations
- CHANGELOG count corrections — System modules 10→9, APCore client methods 19→17
- Phantom entry removed — TypeScript
batchProcessingannotation (never implemented)
- APCore Client API (
docs/api/client-api.md) — Full API reference for the unifiedAPCoreclient covering 17 public methods:call,call_async,stream,validate,module,register,discover,list_modules,describe,use,use_before,use_after,remove,on,off,disable,enable, plusevents/registry/executorproperties and globalapcore.*entry points - Event System (
docs/features/event-system.md) —EventEmitter,ApCoreEvent,EventSubscriberprotocol,WebhookSubscriber(retry strategy),A2ASubscriber(auth modes), subscriber type factory registry, event types table, and YAML configuration reference - System Modules (
docs/features/system-modules.md) — Complete reference for 9 built-insystem.*modules with input/output schemas:system.health.summary,system.health.module,system.manifest.module,system.manifest.full,system.usage.summary,system.usage.module,system.control.update_config,system.control.reload_module,system.control.toggle_feature; plusregister_sys_modules()setup guide and YAML configuration
- Observability features —
ErrorHistory,UsageCollector,PlatformNotifyMiddlewaredocumented - Middleware guide — Built-in
RetryMiddleware+RetryConfigreference added - Schema system —
SchemaStrategyandExportProfileenums documented
docs/getting-started.md— Added §8 Global Entry Points (16apcore.*module-level functions) and §9 System Modules quick start with health/usage/manifest examplesdocs/api/executor-api.md—validate()return type updated fromValidationResulttoPreflightResultwith 6-check breakdown andrequires_approvalflag;call()/call_async()/stream()signatures updated withversion_hintparameter; addedModuleDisabledError,ModuleTimeoutError,ReloadFailedError,FeatureNotImplementedError,DependencyNotFoundErrorto error types; timeout section rewritten for dual-timeout model with cooperative cancellation (CancelToken+ 5s grace period)docs/api/registry-api.md— Addeddisable()/enable()module toggle,safe_unregister()with cooperative drain (Algorithm A21),acquire()context manager,is_draining(),describe()for AI/LLM tool discovery,negotiate_version()(Algorithm A14)docs/features/observability.md— Added three subsystems:ErrorHistory(ring buffer with deduplication,ErrorEntrydataclass),UsageCollector(hourly bucketed storage, trend computation,UsageMiddleware),PlatformNotifyMiddleware(threshold-based alerting with hysteresis)docs/guides/middleware.md— Replaced hand-writtenRetryMiddlewareexample with built-inRetryMiddleware+RetryConfigreference (exponential/fixed backoff, jitter, retryable-only); added §5.5–5.7 cross-references forErrorHistoryMiddleware,UsageMiddleware,PlatformNotifyMiddlewaredocs/features/schema-system.md— AddedSchemaStrategyenum (yaml_first,native_first,yaml_only) andExportProfileenum (mcp,openai,anthropic,generic)docs/features/core-executor.md— Added dual-timeout model (global deadline + per-module), cooperative cancellation withCancelToken, deep merge for streaming (depth cap 32), error propagation viapropagate_error()(Algorithm A11),PreflightResultvalidationdocs/README.md— Addedclient-api.md,event-system.md,system-modules.mdto directory tree, API reference table, feature specifications table, and concept index
- IDConverter implementation note (§12.2) — SDKs MAY implement as utility function instead of class
- MiddlewareManager split-method pattern (§12.2) — SDKs MAY use
execute_before/execute_after/execute_on_errorinstead of unifiedrun_chain() - Module.stream() optional method (§5.6) — Documented
stream()as optional method in Module interface for streaming support
DependencyNotFoundError— New error class forDEPENDENCY_NOT_FOUNDcode (previously forward-declared)FeatureNotImplementedError(Python) /NotImplementedError(TypeScript) — New error class forGENERAL_NOT_IMPLEMENTEDcode (previously forward-declared)
- Executor pipeline docs — All references updated from "10-step" to "11-step" pipeline across README.md, docs/features/core-executor.md, docs/api/executor-api.md
- Context
loggerproperty — Upgraded from SHOULD to MUST in docs/api/context-object.md (both SDKs already provide it) - docs/api/module-interface.md — Added optional
stream()method documentation - **docs/getting-started.md
** — Rewritten to recommendAPCore` unified client as primary approach
- Executor.validate() preflight (§12.2) —
[SHOULD]non-destructive preflight check through Steps 1–6 without invoking module code or middleware; newPreflightResult/PreflightCheckResulttypes with duck-typeValidationResultcompatibility - §12.8 Executor.validate() Cross-Language Implementation Guide — error handling mapping, type mapping for Python/TypeScript/Go/Rust/Java/C/C++, schema library requirements, naming conventions
- Preflight Tests added to §12.4 Consistency Test Suite (7 test cases)
- Context optional extension fields —
cancel_token,services,redacted_inputswith serialization rules (§5.7) - New error codes —
CONFIG_NOT_FOUND,CONFIG_INVALID,SCHEMA_CIRCULAR_REF,BINDING_FILE_INVALID,MIDDLEWARE_CHAIN_ERROR,VERSION_INCOMPATIBLE,ERROR_CODE_COLLISION,CIRCULAR_DEPENDENCY,DEPENDENCY_NOT_FOUND(§8) - New error classes —
BindingFileInvalidError,MiddlewareChainError,VersionIncompatibleError,ErrorCodeCollisionErroradded to error hierarchy - AI error guidance fields —
retryable,ai_guidance,user_fixable,suggestionsfor improved LLM agent error handling - AI intent metadata keys (§4.6) —
x-when-to-use,x-when-not-to-use,x-common-mistakes,x-workflow-hintsconventions for LLM agents - TypeScript and C/C++ added to §12.6 Language-Specific Implementation Notes
PROTOCOL_SPEC.md— bumped to v1.4.0-draft- Executor pipeline renumbered from 10 steps (with Step 4.5) to clean 11 steps — Approval Gate is now Step 5, subsequent steps shifted +1
- §7.4, §7.9, streaming protocol references updated to match new 11-step numbering
- Executor.validate() preflight added to §12.3 cross-language requirements table
- Section cross-references fixed — §11.7→§12.7, §10.3→§11.3, §9.7→§10.7, §7→§8 (error code references)
- Retryability table updated with new error codes;
MIDDLEWARE_CHAIN_ERRORremoved from forward-declared list docs/api/context-object.md— added optional extension fields documentationdocs/api/executor-api.md— added AI error guidance fields and new error typesdocs/concepts.md— expanded AI collaboration and cognitive interface conceptsSCOPE.md— updated to reflect new features
- AI Collaboration Lifecycle documentation: Integrated
description,metadata,requires_approval, andai_guidanceinto a unified narrative (Discovery, Strategy, Governance, Recovery). - New "Cognitive Interface" concept in
README.mdanddocs/concepts.md. - Intent-oriented design tips in
docs/guides/creating-modules.md. - Comprehensive multi-language "Getting Started" guide covering both Python and TypeScript side-by-side
- Multi-language support (side-by-side examples) in "Creating Modules" guide
- Unified documentation links across all implementation READMEs (
apcore-python,apcore-typescript) - TypeScript examples for Registry, Executor, and Module definition in core documentation
README.md— Updated Quick Start with multi-language tabs and links to the new Getting Started guide
- Approval System (§7) — new section in
PROTOCOL_SPEC.mddefining theApprovalHandlerprotocol,ApprovalRequest/ApprovalResultdata types, Executor Step 4.5 integration, error types (APPROVAL_DENIED,APPROVAL_TIMEOUT,APPROVAL_PENDING), built-in handlers, protocol bridge handlers, phased implementation (Phase A sync, Phase B async), and conformance levels
docs/features/approval-system.md— full specification of the Approval System feature
PROTOCOL_SPEC.md— bumped to v1.3.0-draft; added "Recommended AI Intent Metadata Keys" (§4.6) outliningx-when-to-use,x-when-not-to-use,x-common-mistakes, andx-workflow-hintsconventions for LLM agents; updatedrequires_approvalannotation description to reference runtime enforcement; added approval error codes and error hierarchy; renumbered §7–§13 → §8–§14docs/api/executor-api.md— addedapproval_handlerconstructor parameter,ApprovalDeniedError/ApprovalTimeoutErrorto error types, Step 4.5 to execution flow and state machinedocs/api/module-interface.md— updatedrequires_approvalannotation description to reference Approval System and runtime enforcementdocs/features/core-executor.md— added Step 4.5 (Approval Gate) to the execution pipelinedocs/README.md— added Approval System to feature specifications table, directory tree, and concept index
- Streaming execution protocol — new section in
PROTOCOL_SPEC.mddefining streaming execution model for long-running or real-time module outputs - Module ID constraints & naming conventions — formal rules for module identifiers added to the protocol spec
- SDK export requirements — specified required exports for conformant SDK implementations
- Module interface improvements — refined module interface contracts with streaming semantics
docs/features/acl-system.md— full specification of the Access Control List systemdocs/features/core-executor.md— detailed documentation of the execution pipelinedocs/features/decorator-bindings.md— guide on the@moduledecorator and binding mechanicsdocs/features/middleware-system.md— composable middleware pipeline specificationdocs/features/observability.md— tracing, metrics, and structured logging documentationdocs/features/registry-system.md— module registry and discovery system documentationdocs/features/schema-system.md— schema-driven module input/output validation documentation
- GitHub Actions workflow (
.github/workflows/deploy-docs.yml) for automated documentation deployment - MkDocs configuration (
mkdocs.yml) for the documentation site
- README — enhanced with unified SDK explanation, TypeScript SDK implementation examples, and updated architecture overview
- SCOPE.md — updated to reflect current project scope and feature set
docs/concepts.md— rewritten with unified SDK explanation for multi-language contextdocs/architecture.md— updated to align with protocol spec changesdocs/api/— updatedcontext-object.md,executor-api.md,module-interface.md, andregistry-api.mdwith version-aligned contentdocs/spec/conformance.md— revised conformance requirements to match 0.2.0 protocol specdocs/guides/adapter-development.md— updated adapter development guidemkdocs.yml— navigation structure updated to include new feature pages
ROADMAP.md— removed; roadmap references updated across documentationdocs/guides/creating-modules-translated.md— removed translated guide from navigation
- Level 2 Conformance (Phase 1) — Extension system, Async Task Management, and W3C Trace Context support added to protocol requirements
- Extension System (§12.2) — Unified extension point framework for pluggable components (discoverers, middleware, ACL, exporters)
- Async Task Management (§12.7.3) — Standardized lifecycle for background tasks (Pending, Running, Completed, Failed, Cancelled)
- Trace Context (§12.7.4) — W3C Trace Context (traceparent) support for distributed tracing propagation
- Async Middleware Protocol — Requirements for non-blocking middleware dispatch
- Streaming Support — Formalized
ModuleAnnotations.streamingandExecutor.stream()behavior in protocol specification - Shallow Merge for Streaming — Algorithm for accumulating streaming chunks for output validation and post-processing
- ErrorCodes Catalog — Standardized error code constants (replaces hardcoded strings)
- ContextFactory Protocol — Interface for creating Context from platform-specific requests
- Registry Constants — Standardized module ID patterns and event types
- Comprehensive Schema System — Formalized schema loading, validation, and multi-profile export (MCP, OpenAI, Anthropic) requirements
- Module ID Validation — Strengthened pattern to
^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$(lowercase, digits, underscores, dots; no hyphens) - Registry Event Constants — Standardized event names for module registration lifecycle
Initial Release
- Schema-driven modules - Define modules with Pydantic input/output schemas and automatic validation
- @module decorator - Zero-boilerplate decorator to turn functions into schema-aware modules
- Executor - 10-step execution pipeline with comprehensive safety and security checks
- Registry - Module registration and discovery system with metadata support
- Access Control (ACL) - Pattern-based, first-match-wins rule system with wildcard support
- Call depth limits - Prevent infinite recursion and stack overflow
- Circular call detection - Detect and prevent circular module calls
- Frequency throttling - Rate limit module execution
- Timeout support - Configure execution timeouts per module
- Composable pipeline - Before/after hooks for request/response processing
- Error recovery - Graceful error handling and recovery in middleware chain
- LoggingMiddleware - Structured logging for all module calls
- TracingMiddleware - Distributed tracing with span support for observability
- YAML bindings - Register modules declaratively without modifying source code
- Configuration system - Centralized configuration management
- Environment support - Environment-based configuration override
- Tracing - Span-based distributed tracing integration
- Metrics - Built-in metrics collection for execution monitoring
- Context logging - Structured logging with execution context propagation
- Sync/Async modules - Seamless support for both synchronous and asynchronous execution
- Async executor - Non-blocking execution for async-first applications
- Type safety - Full type annotations across the framework
- Comprehensive tests - 90%+ test coverage with unit and integration tests
- Documentation - Quick start guide, examples, and API documentation
- Examples - Sample modules demonstrating decorator-based and class-based patterns