Harness-Config Authoring Guide
July 29, 2026 · View on GitHub
How to build a scion harness-config bundle for a coding-agent CLI. Written for
authors who already know their target tool and are building their first scion
harness — typically shipping the bundle in their own repository and installing
it with scion harness-config install.
The codex bundle is the reference example throughout: it exercises
every part of the system (declarative auth with env + file methods, custom MCP
translation, telemetry reconciliation, a dialect.yaml hook dialect, and a
provision_test.py). Read it side by side with this guide.
What a harness-config is
A harness-config teaches scion how to run a coding-agent CLI (a "harness") inside an agent container. It is a self-contained directory of:
- Declarative metadata (
config.yaml) the host uses to build the CLI command line, select auth, advertise capabilities, and stage inputs. - A container-side provisioner (
provision.py) that runs inside the agent container at the pre-start lifecycle event and performs all harness-native file writes (auth files, client settings, MCP registration, instructions). - A container image (
Dockerfile) with the CLI installed, built on a scion base image. - Seed files (
home/) copied into the agent's home directory.
The split is deliberate: host and broker code never rewrite harness-native
config formats. The host stages universal inputs (instructions, system
prompt, auth candidates, MCP servers, telemetry) into a bundle directory in
the agent home; your provision.py translates them into whatever your tool
natively reads.
Users install a bundle from a local checkout or straight from GitHub:
scion harness-config install harnesses/codex
scion harness-config install github.com/GoogleCloudPlatform/scion/tree/main/harnesses/codex
Installed bundles live at ~/.scion/harness-configs/<name>/ (global) or
.scion/harness-configs/<name>/ (project). Resolution precedence is
template → project → global. Related CLI: scion harness-config list|show|upgrade|reset|sync|pull.
Bundle layout
<name>/
config.yaml # required — harness metadata, parsed and schema-validated by the host
provision.py # container-side provisioner (pre-start)
scion_harness.py # vendored shared library imported by provision.py
capture_auth.py # thin shim: sys.exit(scion_harness.capture_auth_main())
dialect.yaml # optional — data-driven hook-event dialect
home/ # seeded into the agent home at install time
.bashrc
.<tool>/... # native client settings, hook wiring, notify scripts
Dockerfile # image build, FROM ${BASE_IMAGE} (a scion-base)
cloudbuild.yaml # optional — Cloud Build config for the image
provision_test.py # optional — unit tests for provision.py
README.md # install instructions, auth modes, build steps
At install time, provision.py, scion_harness.py, capture_auth.py,
dialect.yaml (plus schema/, examples/, tests/fixtures/) stay at the
bundle root; everything under home/ is copied into the harness-config's
home/ directory verbatim. Dockerfile, cloudbuild.yaml, README.md,
provision_test.py, .gitkeep, and init-firewall.sh are not installed.
Always place seed files under an explicit home/ prefix — files at the bundle
root that aren't recognized support files are routed through a legacy
filename-mapping table you don't want to depend on.
The install machinery computes a content-hash revision over the bundle
(skipping cloudbuild.yaml, README.md, .gitkeep) that is stamped on
agents for audit, so keep generated files deterministic.
config.yaml
Validated on load against the settings-v1 JSON schema (scion will refuse an
invalid bundle). Fields, grouped by concern:
Identity
| Field | Meaning |
|---|---|
harness | Harness type name (required). Also the default --dialect your hooks should use. |
name | Optional override for the config's directory-derived name (no path separators). |
image | Container image tag the runtime launches, e.g. scion-codex:latest. Must match what your Dockerfile builds. |
user | In-container user; use scion (created by scion-base with uid 1000). |
Provisioner
provisioner:
type: container-script
interface_version: 1
lib: vendored
command: ["python3", "/home/scion/.scion/harness/provision.py"]
timeout: 30s
lifecycle_events: [pre-start]
required_image_tools: [python3]
type: container-script is the only supported type for new harnesses.
lib: vendored tells the host to stage your bundle's scion_harness.py
next to provision.py (the alternative, injected, uses the copy embedded
in the scion binary — fine for quick experiments, but vendored gives you a
pinned, reviewable library version). required_image_tools lists binaries
that must exist in the image (python3 always; add jq etc. if your
provision-time hook commands need them).
Command construction
command:
base: ["codex", "--sandbox", "danger-full-access", ...]
task_flag: "--prompt" # omit for positional task argument
task_position: after_base_args # or before_base_args
resume_flag: "resume --last" # split on whitespace into argv tokens
system_prompt_flag: "--system-prompt" # only if the CLI accepts a native flag
The host assembles: base... [resume tokens] [harness args] [task] (default
after_base_args), or base... [resume tokens] [task] [harness args] with
before_base_args. Pick the position that matches how your CLI parses argv
when extra flags are passed through — this is a common parity bug (see the
comments in the codex and opencode configs). resume_flag is whitespace-split,
so multi-token resume subcommands work. If system_prompt_flag is set, the
host appends it with the staged system-prompt content at launch.
Base flags should put the tool in full-permission, non-interactive-approval
mode (--yolo, --dangerously-skip-permissions, approval_policy = never,
etc.) — the agent runs unattended inside a sandboxed container.
Prompts, instructions, skills
| Field | Meaning |
|---|---|
config_dir | Harness-native config directory relative to home, e.g. .codex. |
skills_dir | Where installed skills land, e.g. .codex/skills. |
instructions_file | Home-relative file your provisioner projects scion instructions into, e.g. .codex/AGENTS.md. |
system_prompt_file | Home-relative native system-prompt file, if the tool has one. |
system_prompt_mode | native (tool has a real system-prompt channel), prepend_to_instructions (downgrade into the instructions file), or none. |
All paths must be relative (e.g.
.copilot/skills, not~/.copilot/skills). The Go provisioner joins them withagentHomeviafilepath.Join;~is not expanded and will produce a broken literal path like/home/scion/~/.copilot/skills.
scion_harness.project_instructions() honors system_prompt_mode and
skills_dir from these fields. It projects system prompt and instruction
content by default, but include_skills defaults to False because scion's
host-side provisioner already installs skills as individual files under
skills_dir. Only pass include_skills=True for a harness that has no native
skills directory and must inline SKILL.md content into its instruction file.
Models
model_aliases:
small: gpt-5.4-mini
medium: gpt-5.4
large: gpt-5.5
extra-large: gpt-5.5
Templates reference abstract sizes. Provide all four conventional aliases.
Scion delivers the agent's model to the container in the SCION_MODEL env
var — that is the only live channel. (ctx.model_resolution reads a manifest
key that the Go side never writes; see changelog 2026-07-20. Don't build on
it.) SCION_MODEL may still carry a raw tier name, so resolve it in
provision.py:
raw = os.environ.get("SCION_MODEL", "").strip()
aliases = ctx.harness_config.get("model_aliases") or {}
model = aliases.get(raw.lower(), raw) or "<your default>"
Mirror config.NormalizeModelAlias / config.ResolveModelAlias
(pkg/config/templates.go) when you normalize: Scion also passes
--model <resolved> on the CLI command line when the agent has a model
configured, and a harness-side spelling the Go side does not accept would
make the two disagree. Then write the result to the tool's native settings
file or the env overlay. Do not pin the tool's model env var in the env
block below — see the precedence note there.
Environment
env: static env vars set in the container.env_template: values with placeholder expansion —{{ .AgentName }},{{ .AgentHome }},{{ .UnixUsername }}(only these three).
Precedence: container env (env, env_template, template/CLI env) beats the
env.json overlay your provisioner writes — the overlay only adds keys that
are not already set, so a runtime value can never be masked by a harness
script. That makes the env block the wrong place for anything the
provisioner needs to compute at start time (the selected model, auth-mode
dependent vars); put the default in provision.py instead.
Interrupts
interrupt_key (tmux key, e.g. C-c or Escape), and for tools needing a
multi-key interrupt, interrupt_signal: sequence +
interrupt_sequence: [Escape, C-c] (each entry sent as a separate key).
Capabilities
An honest support matrix consumed by templates, preflight checks, and UIs:
capabilities:
limits:
max_turns: { support: "yes" }
max_model_calls: { support: "no", reason: "no model start/end hook events" }
max_duration: { support: "yes" }
telemetry:
enabled: { support: "yes" }
native_emitter: { support: "yes" }
prompts:
system_prompt: { support: "partial", reason: "downgraded into AGENTS.md" }
agent_instructions: { support: "yes" }
auth:
api_key: { support: "yes" }
auth_file: { support: "yes" }
oauth_token: { support: "no" }
vertex_ai: { support: "no", reason: "..." }
mcp:
stdio: { support: "yes" }
sse: { support: "partial", reason: "mapped to single HTTP type" }
streamable_http: { support: "yes" }
project_scope: { support: "no", reason: "demoted to global" }
resume: { support: "yes" }
Every field takes support: yes|partial|no plus an optional reason (give
one for anything but yes). What each signals:
- limits.max_turns / max_model_calls — enforceable only if your hook
wiring emits the corresponding normalized events (
prompt-submit,model-start/model-end). Don't claim support without the events. - limits.max_duration — wall-clock; effectively always
yes. - telemetry.enabled / native_emitter — whether scion can toggle telemetry and whether the tool emits OTEL natively (vs. scion inferring from hooks).
- prompts.system_prompt —
yesonly for a true native system-prompt channel;partialwhensystem_prompt_mode: prepend_to_instructions. - auth.* — which auth types in your
auth:block actually work. - mcp.* — transports your provisioner translates;
project_scopereflects whether project-scoped servers are honored or demoted to global. - resume — whether
resume_flaggenuinely restores a session.
Auth
auth:
default_type: api-key
types:
api-key:
required_env:
- any_of: ["CODEX_API_KEY", "OPENAI_API_KEY"]
auth-file:
required_files:
- name: CODEX_AUTH
type: file
target_suffix: "/.codex/auth.json"
field: CodexAuthFile
autodetect:
env:
CODEX_API_KEY: api-key
OPENAI_API_KEY: api-key
files:
CODEX_AUTH: auth-file
types.<name>.required_env— list ofany_ofgroups; each group needs at least one var present. Multiple groups = all groups required (e.g. vertex-ai needs a project var AND a region var).types.<name>.required_files— file credentials. Key fields:name: secret key (also the capture-auth key and staging filename).target_suffix: in-container path suffix, e.g./.codex/auth.json; used to match host file mappings and to derive the capture source (~+ suffix).field: the GoAuthConfigfield this file maps to (needed for the hardcoded set of known credential files the host knows how to gather; see Known Gaps).alternative_env_keys: env vars that satisfy the requirement instead of the file (e.g.GOOGLE_APPLICATION_CREDENTIALSfor ADC).skipped_when_gcp_service_account_assigned: drop the requirement when a GCP workload identity is attached.required: broker must source the secret before dispatch (vs. documentary).
autodetect— maps discovered env vars / file secrets to an auth type when the user didn't select one explicitly.
Conventional type names — api-key, oauth-token, auth-file, vertex-ai —
line up with the capabilities.auth matrix; use them.
no_auth
no_auth:
behavior: drop-to-shell
command: codex login --device-auth
message: |
This agent started without credentials.
Run: codex login --device-auth
Then run: python3 /home/scion/.scion/harness/capture_auth.py
When no credentials are found, instead of failing, the agent drops to a shell
with your message. The user logs in interactively, then runs the staged
capture_auth.py, which reads inputs/capture-auth-config.json (generated by
the host from your required_files declarations) and stores each credential
as a project secret via sciontool secret set — so the next agent starts
authenticated.
MCP (declarative mapping)
If your tool's MCP registry is a JSON file with per-server entries that match the universal schema 1:1 (command/args/env/url/headers plus a transport-type field), declare a mapping and skip custom code:
mcp:
global_config_file: .copilot/mcp-config.json # home-relative
global_config_path: mcpServers # dotted path in the JSON
transport_field: type
transport_map:
stdio: local
sse: http
streamable-http: http
Your provision.py then calls scion_harness.apply_mcp_servers_simple(...).
If the native format is TOML or structurally different (codex, opencode),
leave mcp: out and write a translate function using
scion_harness.apply_mcp_translated(...) instead.
dialect (inline)
There is also a free-form dialect: map field in the schema; current bundles
use a separate dialect.yaml file instead (see Hooks below).
Runtime contract: host staging → provision.py
At agent provision time, the host (ContainerScriptHarness) stages a bundle
into the agent home. Inside the container it appears as:
$HOME/.scion/harness/
manifest.json # what/where — read by scion_harness.run()
config.yaml # your config, verbatim
provision.py # your script (chmod 755)
scion_harness.py # staged per provisioner.lib mode
capture_auth.py # if present in the bundle
dialect.yaml # if present in the bundle
inputs/
instructions.md # composed agent instructions (if any)
system-prompt.md # system prompt content (if any)
auth-candidates.json # candidate env vars, secret file paths, explicit type
mcp-servers.json # universal MCP server map (if any configured)
telemetry.json # effective TelemetryConfig + env overlay
resolved-skills.json # resolved skills (if any)
capture-auth-config.json # generated from auth required_files
outputs/ # your script writes here
resolved-auth.json
env.json
secrets/ # 0600 files, one per credential
<ENV_VAR_NAME>
<FILE_SECRET_NAME>
The host also writes a trusted lifecycle hook at
$HOME/.scion/hooks/pre-start.d/20-harness-provision that runs
sciontool harness provision --manifest $HOME/.scion/harness/manifest.json,
which executes your provisioner.command on every agent start/resume. Secret
env vars are stripped from the script's process environment — read secret
values from the staged files in secrets/ (via the ProvisionContext
helpers), never from os.environ.
manifest.json gives you: schema_version, command ("provision"),
agent_name, agent_home, agent_workspace, harness_bundle_dir,
harness_config (your parsed config.yaml — read instructions_file,
system_prompt_mode, etc. from here rather than hardcoding), inputs /
outputs paths, and platform. (ProvisionContext.model_resolution reads a
model_resolution key that ProvisionManifest does not emit — it is always
empty; use SCION_MODEL instead.)
What provision.py must do
- Select an auth method (
ctx.select_auth(AUTH)) and write the tool's native credential file(s) or env overlay. - Project instructions:
scion_harness.project_instructions(ctx, instructions_file). Leaveinclude_skillsat its defaultFalsewhen your config declaresskills_dir; passinclude_skills=Trueonly for harnesses that cannot load skills from files. - Translate MCP servers (
apply_mcp_servers_simpleorapply_mcp_translated). - Reconcile telemetry into native client settings if the tool supports it
(
ctx.telemetry). - Any other native writes: trust the workspace, pre-approve keys, disable auto-update/onboarding prompts, generate hook files.
- Write outputs:
ctx.write_outputs(resolved, env={...})—resolved-auth.jsonrecords the chosen method;env.jsonmaps env vars to project into the harness process (values may be literal or${VAR}placeholders resolved from the environment at launch).
Keep the script stdlib-only and idempotent — it runs on every start and resume. Use the library's atomic write and managed-marker helpers; never clobber user edits outside your managed regions.
Minimal skeleton (see codex/provision.py for the real thing):
import scion_harness
AUTH = scion_harness.AuthSpec("mytool", [
scion_harness.env_method("api-key", any_of=["MYTOOL_API_KEY"]),
scion_harness.file_method("auth-file", path="~/.mytool/auth.json",
secret_key="MYTOOL_AUTH"),
])
def provision(ctx: scion_harness.ProvisionContext) -> None:
resolved = ctx.select_auth(AUTH)
# ... write native auth/config files ...
scion_harness.project_instructions(
ctx, str(ctx.harness_config.get("instructions_file") or "AGENTS.md"))
ctx.write_outputs(resolved, env={})
if __name__ == "__main__":
scion_harness.run("mytool", provision)
scion_harness.py
The shared, stdlib-only library. Vendor it: copy the canonical
harnesses/scion_harness.py from the scion repo into your bundle, set
provisioner.lib: vendored, and update your copy manually when you pick up
new library features. Guard against staleness at the top of provision.py:
assert scion_harness.INTERFACE_VERSION >= 2
(Bundles inside the scion repo itself are kept in sync mechanically via
go generate ./harnesses/, which stamps a GENERATED FILE header; external
bundles just track the canonical file.)
Key API surface:
run(harness_name, provision_fn)— entry-point scaffold: parses--manifest, loads it, dispatches theprovisioncommand, mapsProvisionError/OSErrorto exit code 1 (EXIT_UNSUPPORTED = 2for unknown commands).ProvisionContext— properties:bundle_dir,inputs_dir,home,workspace,harness_config,candidates,explicit_type,env_keys,file_paths,env_secret_files,file_secret_files,telemetry,model_resolution(always empty — see above). Methods:read_secret(name)/read_file_secret(name)(staged secret values, trailing newline stripped),read_input_text(name),select_auth(spec),write_outputs(resolved, env=, extra=),info()/warn()(stderr).- Auth engine —
AuthSpec(harness, [methods])withenv_method(name, any_of=/all_of=, hint=, env_fallback=)andfile_method(name, path=, secret_key=, hint=).select_authhonors an explicit user selection, otherwise tries methods in declaration order (your precedence), checking staged candidates/secrets before any env fallback; raisesProvisionErrorwith the accumulated hints if nothing matches, or returnsResolvedAuth(method="none")whenno_authbehavior is configured and no candidates were staged. project_instructions(ctx, target, ...)— composesinputs/system-prompt.md(persystem_prompt_mode),inputs/instructions.md, and, only wheninclude_skills=True, installedSKILL.mdfiles fromskills_dirinto the target file inside<!-- BEGIN/END SCION MANAGED -->markers, preserving user content outside the block.include_skillsdefaults toFalse; keep it that way for harnesses that declareskills_dir, because those skills are installed as native files by the host-side provisioner.- MCP —
apply_mcp_servers_simple(bundle_path, mcp_mapping, workspace)for JSON-native tools (driven by themcp:block);apply_mcp_translated(ctx, translate_fn, write_fn)for custom formats (warn-and-skip per server, demotes project scope to global). - File helpers —
atomic_write_json,atomic_write_text(tmp +os.replace),expand_path,load_json,read_json_skipping_comment_lines; TOML emit/reconcile helpers:toml_escape,toml_inline_table,toml_string_array,strip_toml_sections(tomllib is read-only, so TOML editing is manual). capture_auth_main()— the whole capture-auth flow; yourcapture_auth.pyis a two-line shim around it. Exit codes: 0 captured, 1 error, 2 no credentials found, 3 conflict (secret exists;--force).ProvisionError— raise for any fatal provisioning condition.
Hooks and dialect.yaml
Scion observes the agent through hook events: your harness invokes
sciontool hook --dialect=<name> with a JSON event payload on stdin. The
event is normalized and drives status, logging, limits enforcement, Hub
updates, and telemetry.
Three wiring patterns, by how configurable your tool is:
- Static config in
home/— claude and gemini-cli ship hook wiring in their seeded settings files, piping every lifecycle event tosciontool hook --dialect=...(claude/home/.claude/settings.json). - Notify/wrapper script in
home/— codex routes both its legacynotifycallback and its hooks throughscion_notify.sh, which also turns turn-completion intosciontool status task_completed. - Generated at provision time — when hook config must contain runtime
values or live in the workspace, write it from provision.py (antigravity
generates
/workspace/.agents/hooks.jsonwiring events throughjqintosciontool hook --dialect=antigravity).
sciontool has built-in dialects for claude, gemini, and codex. For any
other harness, ship a dialect.yaml in the bundle — it is staged to
$HOME/.scion/harness/dialect.yaml, and sciontool hook loads it when its
dialect: name matches the --dialect flag (a bundled dialect also overrides
a built-in of the same name):
dialect: mytool
event_name_fields: [hook_event_name, type, event] # tried in order
mappings:
BeforeTool:
event: tool-start
fields: # optional dotted-path field extraction
tool_name: toolCall.name
tool_input: toolCall.args
AfterAgent:
event: agent-end
Map into the normalized event vocabulary: session-start, session-end,
prompt-submit, tool-start, tool-end, model-start, model-end,
agent-end, subagent-end, response-complete, notification.
Extractable fields include prompt, tool_name, tool_input,
tool_output, message, session_id, success, error, assistant_text,
file_path, and input_tokens / output_tokens / cached_tokens.
Your capabilities.limits claims must match this wiring: max_turns needs
prompt-submit, max_model_calls needs model start/end events.
Agent-side status signaling (sciontool status task_completed|blocked|ask_user)
is usually driven by agent instructions/templates, but a hook script may emit
it directly where the tool has a reliable completion callback (codex).
Dockerfile and image pipeline
Every scion harness image must be built on a scion base image — the base
provides the scion user (uid 1000, zsh), sciontool (the container
entrypoint is sciontool init --), python3, node/npm with a shared global
prefix, and the standard developer toolchain. Take the base as a build arg:
# syntax=docker/dockerfile:1
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
ARG MYTOOL_VERSION=latest
# Create the tool's config + skills dirs owned by the agent user.
RUN mkdir -p /home/scion/.mytool/skills && \
chown -R scion:scion /home/scion/.mytool
# Install the CLI on the standard PATH; clean caches.
RUN npm install -g @acme/mytool@${MYTOOL_VERSION} \
&& npm cache clean --force
CMD ["mytool"]
Rules of thumb:
- Never hardcode the base — accept
BASE_IMAGEso users can pin a registry/tag (scion-base:latestlocally, or the publishedus-central1-docker.pkg.dev/.../scion-base:<tag>). - Own what the agent writes: pre-create the tool's config dir and any
cache/state dirs and
chown scion:scionthem. Root-owned surprises in$HOMEare the most common first-run failure. - Do root-level work (
apt-get, sudoers entries, firewall scripts) asUSER root; the entrypoint handles dropping privileges at runtime — don't end with aUSERdirective that breakssciontool init. - Install the CLI at a pinned or arg-controlled version; symlink into
/usr/local/binif the installer doesn't. - Tag the result to match
config.yaml'simage:field.
Local build:
docker build --build-arg BASE_IMAGE=scion-base:latest \
-t scion-mytool:latest -f Dockerfile .
For published images, ship a cloudbuild.yaml following the codex pattern:
buildx multi-arch (linux/amd64,linux/arm64), --build-arg BASE_IMAGE=$_REGISTRY/scion-base:$_TAG, tags for both $_SHORT_SHA and
$_TAG, --pull --push. See codex/cloudbuild.yaml
and image-build/ in the scion repo for how the base images themselves are
built.
home/ seed files
Everything under home/ is copied into the agent home. Use it for:
.bashrcsourcing scion env.- Native client settings that are static: disable auto-update, onboarding
nags, and telemetry prompts; set
approval_policy/yolo mode; pre-trust/workspace; wire hooks (patterns above). - Notify/hook shell scripts.
Anything that depends on runtime values (workspace path, model, credentials,
telemetry endpoint) belongs in provision.py, not in a seed file.
Testing and iteration
- Unit-test provision.py with a fixtures-based
provision_test.py(pattern: codex/provision_test.py) — fake a bundle dir withmanifest.json+inputs/, runprovision(), assert on the native files andoutputs/. - Install locally:
scion harness-config install ./path/to/bundle(this also schema-validatesconfig.yaml), then create an agent with the config and watch the pre-start provision log. - Re-run in place: inside a container,
python3 ~/.scion/harness/provision.py --manifest ~/.scion/harness/manifest.jsonre-executes provisioning against the staged inputs. - Test hook mapping by piping a captured payload:
echo '{"hook_event_name": "BeforeTool", ...}' | sciontool hook --dialect=mytool.
New-harness checklist
config.yaml: identity, provisioner block, command block, model aliases, instructions/system-prompt fields, honest capabilities, auth types + autodetect,no_authdrop-to-shell.- Vendor
scion_harness.py; writeprovision.py(auth → native files, instructions projection, MCP, telemetry, outputs); add the two-linecapture_auth.py. dialect.yaml+ hook wiring (static, notify script, or generated) if the tool exposes lifecycle events; aligncapabilities.limits.home/seeds:.bashrc, native settings with auto-update/onboarding disabled and full-permission mode.Dockerfileon${BASE_IMAGE}, config dirs chowned toscion;cloudbuild.yamlif publishing.provision_test.py,README.md(install command, auth-mode table, build instructions).- Install, create an agent per auth mode you claim to support, and verify:
command line, resume, instructions file content, MCP registration, hook
events,
no_auth→ login →capture_auth.pyround trip.
Known Gaps & Future Work
Current limitations of the harness-config system — listed here so the guidance above stays aspirational-free. Don't design around these as if permanent; check whether they've been fixed.
- Project-scoped MCP is not implemented for most harnesses;
apply_mcp_translateddemotes project-scoped servers to global with a log line. Claude's simple mapping is the only project-scope path exercised. - Version numbering is not unified:
provisioner.interface_versionin config.yaml (1), the manifestschema_version(1), andscion_harness.INTERFACE_VERSION(2) are three separate counters. Guard on the library'sINTERFACE_VERSION; the other two are effectively constant today. outputs/status.jsonis declared in the manifest outputs but nothing writes or reads it yet.- Root-level seed-file mapping is hardcoded: bundle files outside
home/that aren't recognized support files are placed via a per-filename switch (config.toml→.codex/, etc.). Thehome/prefix path is the supported, generic mechanism. required_files.fieldcouples to compiled-in Go struct fields (AuthConfig), so fully novel file credentials for out-of-tree harnesses may not be gathered host-side without a scion change; env-based secrets and the capture-auth flow are the portable path (antigravity'sAGY_TOKENworks this way).- capture-auth source derivation assumes the credential lives at
~+target_suffixinside the container; credentials at other locations need a customcapture_auth.py. - MappingDialect has no per-field source fallbacks — one dotted path per
field (codex's hyphenated
last-assistant-messagelegacy key can't be expressed alongside the new one). sciontool hook --dialecthelp text only advertises the built-in dialects; bundleddialect.yamldialects work but are undocumented there.- Vendored-lib drift for external bundles is manual: nothing warns when
your vendored
scion_harness.pyfalls behind the canonical copy (in-repo bundles are covered bygo generate+ a sync test). The host logs the stagedLIB_VERSIONat provision time — check it when debugging. HasSystemPromptchecks for the native system-prompt file on disk, but the file is only written at pre-start, so host-side checks before first start can misreport.- Model alias keys (
small/medium/large/extra-large) are convention, not schema-enforced; missing aliases fail at resolution time, not install time.