projection_marker — AgentTeamsModule¶
The honest team marker an interop or multi_sync projection writes (2026-10-01). Native generation
records <agents dir>/references/build-log.json; fleet discovery, the framework-freshness scan,
--check, --verify-integrity, the Claude block's present-sibling denies and the confine-run.sh
launcher all key on that file. Before this module, no interop path wrote one, so a projected
.codex/agents team was invisible to all of them.
Source:
agentteams/projection_marker.py
Constants¶
MARKER_SCHEMA_VERSION—"1.5", the native build-log's schema version.INTEROP_ORIGIN—"interop", theoriginvalue that identifies a projection marker (alsodrift.INTEROP_ORIGIN).MARKER_FRAMEWORKS—codex,copilot-vscode,copilot-cli,goose. Claude is excluded: a claude marker makes the launcher require the gate hook, which only native generation emits. agents-md and canonical have no team-marker convention.
Functions¶
write_projection_marker(agents_dir, framework, *, source_dir, source_framework, files_written, agent_slugs=None) -> ProjectionMarkerResult¶
Writes, or refreshes, the origin: "interop" marker of a projected team. The marker carries
schema_version, agentteams_version, generated_at, project_name and agent_slug_list (from
the source team's build-log when it has one, else agent_slugs), framework, files_written
(project-root-relative), file_hashes (16-character SHA-256, agents-dir-relative, the native
format, so --verify-integrity reads them), template_hashes: {}, origin, source_dir
(project-relative when inside the project), source_framework and source_build_log_sha256.
- It never overwrites a native build-log. That means one without
origin: "interop", an unparseable one, or a symlink. An existing interop marker is refreshed. When nothing was projected, an existing interop marker is left as it is. - The control plane is written first (security condition 12). Once the marker exists, the
launcher requires the team's switch, verify-key store and rosters. They are written in this
order (the sentinel and stubs write-if-absent,
O_CREAT|O_EXCL|O_NOFOLLOW): - the switch, which mirrors the source team's
enforce_decision_signing. Since 2026-10-11 it is not write-if-absent:signing_switch.apply_switchrewrites a switch the pipeline owns on every projection, including one that projected no files, keeps one of unclear ownership, and prints a fleet REVIEW notice for atrue -> falsechange. A missing source switch givesfalse, which reads like no switch. A source switch that is a symlink, not a regular file, unreadable, malformed or non-boolean refuses the marker, and no switch is written. When the source or target team hassigning-governed.marker, the switch is alwaystrue, and an existingfalsetarget switch refuses the marker; - the verify-key store sentinel;
- the roster stubs (
control_plane_io.write_control_plane_stubs).
Each required entry is then checked again: it must be a real file or directory, never a symlink.
- No marker is written on any failure. If a write fails, a path is unsafe, the agents dir, its
parent, references/ or the store is a symlink, or the marker path is a symlink, error is set.
- The write is atomic. references/ is lstat-checked and opened O_DIRECTORY|O_NOFOLLOW.
The temp file and the os.replace are relative to that descriptor. The existing marker's origin
is checked again just before the replace, so a native build-log that appears meanwhile is kept.
Callers:
interop.run_interop(re-exported asinterop.write_projection_marker), after a real run with no errors. A dry run or a skills-only run writes no marker. A refused marker becomes aresult.errorsentry.multi_sync._project_and_rebaseline, after each framework's projection. A refused marker is printed and added to the notices.
Returns a ProjectionMarkerResult with these fields:
marker: the marker path, orNone;control_plane: the files created before the marker;skipped: why nothing was written;error: why the marker was refused;created: true when no marker existed before;notices: switch notices (relaxed, kept, tightened), printed byprint_marker_outcome.
existing_marker_origin(agents_dir) -> str | None¶
Returns None when there is no build-log, "interop" for a projection marker, and "native"
for anything else, including an unparseable file or a symlink.
safe_source_hint(value) -> str¶
Returns a marker's source_dir in a form safe to paste into an --overwrite command. The value
is used only when it is exactly .github/agents, .claude/agents or .goose/recipes, and is then
shell-quoted. Anything else becomes <source>, because the marker is agent-writable. The fleet and
bridge refresh hints use it.
codex_session_advisory(project_root) -> str¶
The text of security condition 14. Once .codex is a recognised team, a Claude sandbox block
generated from then on write-denies .codex whole, so an interop into .codex/agents run from
Bash inside a Claude session fails. The projection must run outside the session. run_interop
adds this as a notice the first time it creates a codex marker. Generation prints it through
generate_helpers._advise_codex_projection_outside_session when a Claude sandbox is on and
.codex/agents carries an interop marker.
print_marker_outcome(result, *, label="") -> None¶
Prints one line for a ProjectionMarkerResult. An error goes to stderr and says that no marker
was written.
The marker authorises nothing (security condition 13)¶
drift.is_interop_marker(build_log) identifies the marker, and every reader treats it as follows:
drift.detect_driftsetsDriftReport.unverifiableto"unverifiable (interop projection: no template hashes)", and--checkfails on it rather than report clean.framework_freshnessreports the same status, and never as "current" or the freshest render.--updatetreats the marker as no prior build.emit._unmodified_since_buildreturns no "unmodified" files, so the marker'sfile_hashesnever let a merge overwrite a file.cli/output_targetdoes not count it as evidence of an agentteams tree.fleetreports an interop Codex team as a SKIP row with its interop refresh command and never runs--updateon it.bridge.native_team_noticegives the interop command.- The launcher's missing-entry message names the interop re-run for
.codex/agents.