Skip to content

Commit 229f55b

Browse files
committed
feat(materialize): declare deterministic file modes
1 parent fea6ff2 commit 229f55b

8 files changed

Lines changed: 431 additions & 96 deletions

File tree

README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,9 +324,29 @@ presence is checked only for the selected run host, which defaults to the local
324324
bare identity or the fully-qualified `<host>.<identity>` bus id.
325325

326326
Materialization simulates all content operations before writing. It refuses any real change to a
327-
Git-tracked target, including `AGENTS.md`; byte-identical tracked content is accepted. Inspect the
328-
declared targets and keep generated overlays untracked. Detection invokes `git` and fails closed if
329-
the executable is unavailable or a workspace that appears to be a worktree cannot be inspected.
327+
Git-tracked target, including a mode-only change; byte-identical content with the declared mode is
328+
accepted. Inspect the declared targets and keep generated overlays untracked. Detection invokes
329+
`git` and fails closed if the executable is unavailable or a workspace that appears to be a worktree
330+
cannot be inspected.
331+
332+
Each content directive accepts `executable=#true`. st2 applies exact mode `0755` when the property is
333+
true and exact mode `0644` when it is absent or false. Materialization repairs a wrong existing mode
334+
even when the file bytes already match. A `copy` source mode does not affect the destination mode.
335+
An operation whose bytes and mode already match is not reported as materialized.
336+
337+
Inline content uses the existing `file` directive. st2 writes the decoded KDL string without adding
338+
or removing a newline. A blank line before a multiline string's closing delimiter encodes one final
339+
newline:
340+
341+
```kdl
342+
file ".st2/bin/probe" executable=#true {
343+
content #"""
344+
#!/bin/sh
345+
printf 'ready\n'
346+
347+
"""#
348+
}
349+
```
330350

331351
`git-exclude` is advisory. `copy`, `file`, `json-upsert`, and `ensure-line` are boot-gating.
332352

docs/vrs/02-agent-spec/spec.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,14 @@ st2 `9887b28` predate Resource bindings. Current st2 source:
264264
<h3 id="f08">F08 <code>render {}</code> operation, template, or resolved target</h3>
265265

266266
Prove ownership for every affected local owner before writing. Conflicts refuse
267-
all affected owners. Write only changed bytes, then notify survivors that can
268-
see the committed target. Unchanged bytes do not notify. Deletion needs explicit
269-
desired state and ownership and never removes a catalog source declaration.
267+
all affected owners. Write changed bytes and enforce the declared mode. The
268+
`executable=#true` property selects exact mode `0755`; absence or false selects
269+
exact mode `0644`. A copy source mode has no effect. Mode-only drift is a
270+
change. Notify survivors that can see the committed target. Matching bytes and
271+
mode do not notify and do not report a materialization. Inline `file` content
272+
uses the decoded KDL string without an added or removed newline. Deletion needs
273+
explicit desired state and ownership and never removes a catalog source
274+
declaration.
270275

271276
Authoring: [pinned render contract][evals-render]. st2 implementation and
272277
evidence: [materializer](../../../src/materialize.rs).

examples/native/README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,15 @@ composer heuristic.
5555

5656
The `render { ... }` block is ordered. `copy`, `file`, `json-upsert`, and `ensure-line` are
5757
boot-gating operations; a failure prevents that agent from starting. Materialization refuses any
58-
real change to a Git-tracked target before its first workspace write. A byte-identical tracked
59-
target is safe and idempotent; untracked and non-Git targets remain writable. `git-exclude` is
60-
advisory, so a non-Git workspace or exclusion failure does not prevent a boot.
58+
content or mode change to a Git-tracked target before its first workspace write. A byte-identical
59+
tracked target with the declared mode is safe and idempotent. Untracked and non-Git targets remain
60+
writable. `git-exclude` is advisory, so a non-Git workspace or exclusion failure does not prevent a
61+
boot.
62+
63+
Each content directive accepts `executable=#true`. The property selects exact mode `0755`; its
64+
absence or false value selects exact mode `0644`. Materialization corrects mode drift even when the
65+
bytes already match. A `copy` source mode does not affect the destination mode. An unchanged
66+
operation is not reported as materialized.
6167

6268
Tracked-target detection invokes `git` and fails closed if it cannot inspect a workspace that appears
6369
to belong to a Git worktree.

src/catalog_transaction.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,7 @@ fn normalize_agent(spec: &agent_spec::AgentSpec) -> Result<BTreeMap<String, Sema
876876
RenderOp::Copy {
877877
source,
878878
destination,
879+
executable,
879880
} => {
880881
insert_value(
881882
&mut fields,
@@ -895,10 +896,17 @@ fn normalize_agent(spec: &agent_spec::AgentSpec) -> Result<BTreeMap<String, Sema
895896
SemanticType::String,
896897
destination,
897898
);
899+
insert_default_bool(
900+
&mut fields,
901+
&format!("{root}/executable"),
902+
*executable,
903+
false,
904+
);
898905
}
899906
RenderOp::File {
900907
destination,
901908
content,
909+
executable,
902910
} => {
903911
insert_value(
904912
&mut fields,
@@ -918,10 +926,17 @@ fn normalize_agent(spec: &agent_spec::AgentSpec) -> Result<BTreeMap<String, Sema
918926
SemanticType::String,
919927
content,
920928
);
929+
insert_default_bool(
930+
&mut fields,
931+
&format!("{root}/executable"),
932+
*executable,
933+
false,
934+
);
921935
}
922936
RenderOp::JsonUpsert {
923937
destination,
924938
content,
939+
executable,
925940
} => {
926941
insert_value(
927942
&mut fields,
@@ -945,8 +960,18 @@ fn normalize_agent(spec: &agent_spec::AgentSpec) -> Result<BTreeMap<String, Sema
945960
SemanticType::String,
946961
&normalized,
947962
);
963+
insert_default_bool(
964+
&mut fields,
965+
&format!("{root}/executable"),
966+
*executable,
967+
false,
968+
);
948969
}
949-
RenderOp::EnsureLine { destination, line } => {
970+
RenderOp::EnsureLine {
971+
destination,
972+
line,
973+
executable,
974+
} => {
950975
insert_value(
951976
&mut fields,
952977
&format!("{root}/kind"),
@@ -965,6 +990,12 @@ fn normalize_agent(spec: &agent_spec::AgentSpec) -> Result<BTreeMap<String, Sema
965990
SemanticType::String,
966991
line,
967992
);
993+
insert_default_bool(
994+
&mut fields,
995+
&format!("{root}/executable"),
996+
*executable,
997+
false,
998+
);
968999
}
9691000
RenderOp::GitExclude { path } => {
9701001
insert_value(

0 commit comments

Comments
 (0)