Skip to content

Commit 33c7505

Browse files
authored
Can render an executable (#329)
* test(materialize): expose mode drift * feat(materialize): declare deterministic file modes
1 parent d4ba08c commit 33c7505

8 files changed

Lines changed: 698 additions & 97 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,11 +926,18 @@ 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,
925939
arrays,
940+
executable,
926941
} => {
927942
insert_value(
928943
&mut fields,
@@ -954,8 +969,18 @@ fn normalize_agent(spec: &agent_spec::AgentSpec) -> Result<BTreeMap<String, Sema
954969
"union",
955970
);
956971
}
972+
insert_default_bool(
973+
&mut fields,
974+
&format!("{root}/executable"),
975+
*executable,
976+
false,
977+
);
957978
}
958-
RenderOp::EnsureLine { destination, line } => {
979+
RenderOp::EnsureLine {
980+
destination,
981+
line,
982+
executable,
983+
} => {
959984
insert_value(
960985
&mut fields,
961986
&format!("{root}/kind"),
@@ -974,6 +999,12 @@ fn normalize_agent(spec: &agent_spec::AgentSpec) -> Result<BTreeMap<String, Sema
974999
SemanticType::String,
9751000
line,
9761001
);
1002+
insert_default_bool(
1003+
&mut fields,
1004+
&format!("{root}/executable"),
1005+
*executable,
1006+
false,
1007+
);
9771008
}
9781009
RenderOp::GitExclude { path } => {
9791010
insert_value(

0 commit comments

Comments
 (0)