sayfirst-cli 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/.github/workflows/ci.yml +132 -33
  2. sayfirst_cli-0.3.0/CHANGELOG.md +165 -0
  3. sayfirst_cli-0.3.0/PKG-INFO +286 -0
  4. sayfirst_cli-0.3.0/QUICKSTART.md +450 -0
  5. sayfirst_cli-0.3.0/README.md +265 -0
  6. sayfirst_cli-0.3.0/docs/PACKS.md +408 -0
  7. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/docs/PARTITION.md +10 -9
  8. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/pyproject.toml +7 -7
  9. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/scripts/gate.sh +19 -13
  10. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/approvals.py +16 -9
  11. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/ask.py +18 -9
  12. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/evidence.py +80 -14
  13. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/exit_codes.py +4 -4
  14. sayfirst_cli-0.3.0/src/sayfirst_cli/instrument/_bootstrap/sitecustomize.py +106 -0
  15. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/instrument/commands.py +155 -10
  16. sayfirst_cli-0.3.0/src/sayfirst_cli/instrument/designation.py +111 -0
  17. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/instrument/engine.py +27 -13
  18. sayfirst_cli-0.3.0/src/sayfirst_cli/instrument/follow.py +156 -0
  19. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/instrument/harness.py +756 -82
  20. sayfirst_cli-0.3.0/src/sayfirst_cli/instrument/interpreter.py +480 -0
  21. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/instrument/launch.py +173 -10
  22. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/instrument/verify.py +110 -29
  23. sayfirst_cli-0.3.0/src/sayfirst_cli/packs/__init__.py +11 -0
  24. sayfirst_cli-0.3.0/src/sayfirst_cli/packs/http-client/interpose.py +233 -0
  25. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/packs/subprocess/interpose.py +12 -5
  26. sayfirst_cli-0.3.0/src/sayfirst_cli/packs/subprocess/pack.toml +39 -0
  27. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/packs_cmd.py +30 -38
  28. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/pages.py +7 -1
  29. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/reads.py +63 -7
  30. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/render.py +5 -1
  31. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/trace.py +10 -6
  32. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/canned_daemon.py +15 -1
  33. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/conftest.py +25 -0
  34. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/contract_absence.py +70 -14
  35. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/documents.py +77 -12
  36. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_approvals.py +32 -2
  37. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_contract_absence.py +116 -2
  38. sayfirst_cli-0.3.0/tests/test_contract_words.py +40 -0
  39. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_decided_name.py +24 -1
  40. sayfirst_cli-0.3.0/tests/test_default_socket.py +211 -0
  41. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_engine.py +57 -0
  42. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_evidence_export_and_exports.py +34 -5
  43. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_evidence_history_and_audit.py +96 -9
  44. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_evidence_reads.py +3 -2
  45. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_exit_codes.py +1 -1
  46. sayfirst_cli-0.3.0/tests/test_follow_children.py +404 -0
  47. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_gate_modes.py +133 -28
  48. sayfirst_cli-0.3.0/tests/test_http_client_pack.py +563 -0
  49. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_instrument_run.py +299 -4
  50. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_instrument_verify.py +295 -11
  51. sayfirst_cli-0.3.0/tests/test_interpreter_target.py +596 -0
  52. sayfirst_cli-0.3.0/tests/test_pack_designation.py +144 -0
  53. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_packs_cmd.py +27 -2
  54. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_packs_shipped.py +1 -0
  55. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_reads.py +4 -3
  56. sayfirst_cli-0.3.0/tests/test_review_bounds.py +111 -0
  57. sayfirst_cli-0.3.0/tests/test_verification_claims_what_it_established.py +541 -0
  58. sayfirst_cli-0.3.0/tests/test_verify_asks_every_effect.py +82 -0
  59. sayfirst_cli-0.3.0/tests/test_verify_pairs_an_inner_spawn.py +241 -0
  60. sayfirst_cli-0.2.0/CHANGELOG.md +0 -44
  61. sayfirst_cli-0.2.0/PKG-INFO +0 -297
  62. sayfirst_cli-0.2.0/QUICKSTART.md +0 -345
  63. sayfirst_cli-0.2.0/README.md +0 -276
  64. sayfirst_cli-0.2.0/docs/PACKS.md +0 -187
  65. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/__init__.py +0 -10
  66. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/http-client/interpose.py +0 -72
  67. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/subprocess/pack.toml +0 -13
  68. sayfirst_cli-0.2.0/tests/test_http_client_pack.py +0 -294
  69. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/.github/workflows/release.yml +0 -0
  70. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/.gitignore +0 -0
  71. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/LICENSE +0 -0
  72. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/NOTICE +0 -0
  73. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/SECURITY.md +0 -0
  74. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/TRADEMARKS.md +0 -0
  75. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/docs/EVIDENCE-SURFACE.md +0 -0
  76. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/docs/PROVENANCE.md +0 -0
  77. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/docs/publication-checklist.md +0 -0
  78. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/scripts/check_dependency_closure.py +0 -0
  79. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/scripts/check_developer_certificate_of_origin.py +0 -0
  80. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/scripts/release_version.py +0 -0
  81. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/__init__.py +0 -0
  82. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/explain.py +0 -0
  83. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/instrument/__init__.py +0 -0
  84. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/instrument/manifest.py +0 -0
  85. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/main.py +0 -0
  86. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/packs/database/NOTE.md +0 -0
  87. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/packs/database/interpose.py +0 -0
  88. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/packs/database/pack.toml +0 -0
  89. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/packs/http-client/NOTE.md +0 -0
  90. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/packs/http-client/pack.toml +0 -0
  91. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/src/sayfirst_cli/packs/subprocess/NOTE.md +0 -0
  92. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/governed_programs.py +0 -0
  93. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/pack_doubles.py +0 -0
  94. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/replies.py +0 -0
  95. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_ask_connection_loss.py +0 -0
  96. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_ask_end_to_end.py +0 -0
  97. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_boundary_runtime.py +0 -0
  98. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_changelog_names_the_version.py +0 -0
  99. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_database_pack.py +0 -0
  100. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_dependency_closure.py +0 -0
  101. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_developer_certificate_of_origin.py +0 -0
  102. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_engine_is_agnostic.py +0 -0
  103. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_governed_run_leaves_no_trace.py +0 -0
  104. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_manifest.py +0 -0
  105. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_offline_audit_vectors.py +0 -0
  106. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_pack_doubles.py +0 -0
  107. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_partition_boundary.py +0 -0
  108. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_pointers_survive_publication.py +0 -0
  109. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_public_vocabulary.py +0 -0
  110. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_publication_checklist_names_every_act.py +0 -0
  111. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_quickstart_names_what_exists.py +0 -0
  112. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_read_rendering.py +0 -0
  113. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_release_workflow_publishes_only_on_a_tag.py +0 -0
  114. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_running_the_module.py +0 -0
  115. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_shipped_wheel_notices.py +0 -0
  116. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_source_distribution_notices.py +0 -0
  117. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_spdx_identifiers.py +0 -0
  118. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_subprocess_pack.py +0 -0
  119. {sayfirst_cli-0.2.0 → sayfirst_cli-0.3.0}/tests/test_trace_and_explain.py +0 -0
@@ -6,9 +6,9 @@
6
6
  # run on a machine has to be given. Apart from it, what a contributor runs
7
7
  # locally is what decides a merge; a step that exists only here is a step nobody
8
8
  # can reproduce, and `tests/test_gate_modes.py` fails if this file grows a
9
- # second one. The rest of what is written below decides which of the gate's two
10
- # green outcomes this run reached and renders it, which is a rendering job
11
- # rather than a check.
9
+ # second one. The rest of what is written below decides which leg of the gate
10
+ # this run is entitled to and renders which of its two green outcomes it
11
+ # reached, which is routing and rendering rather than a check.
12
12
  #
13
13
  # This workflow publishes nothing. `release.yml` is the only file here that can,
14
14
  # and only on a tag; a publish step added here is a constitutional change, not a
@@ -33,17 +33,19 @@
33
33
  # text: neither file spells the version, because a version spelled twice is a
34
34
  # version that will eventually be spelled two ways.
35
35
  #
36
- # WHY A REDUCED LEG STILL RUNS, AND NOW RUNS ON EVERY RUN. A machine that cannot
37
- # reach that repository still exists — a fork with no network, a runner behind a
38
- # proxy, a contributor offline — and what it gets is a contract that is simply
36
+ # WHY A REDUCED LEG STILL EXISTS. A machine that cannot reach that repository
37
+ # still exists — a fork with no network, a runner behind a proxy, a contributor
38
+ # offline — and what it gets is a contract that is simply
39
39
  # absent. An absent contract is a fact about the world, not a crash. Article 2
40
40
  # says an absence is "never rendered as a negative fact, a zero or a healthy
41
41
  # state", so `scripts/gate.sh` has three outcomes and this workflow renders
42
42
  # three and not two:
43
43
  #
44
- # gate (full) the contract was built from the control
44
+ # gate (full, python X.Y) the contract was built from the control
45
45
  # plane's public repository, at the tag this
46
- # client pins, and every check ran.
46
+ # client pins, and every check ran — once per
47
+ # interpreter `requires-python` admits, since
48
+ # a release installs on any of them.
47
49
  # `scripts/gate.sh` exits 0.
48
50
  # gate (reduced — contract no checkout of the control plane was made,
49
51
  # absent, some checks not run) so the contract is absent. Format, lint and
@@ -57,16 +59,36 @@
57
59
  # gate came back reduced, and a reduced job
58
60
  # whose gate came back full.
59
61
  #
60
- # The two legs run side by side now, rather than one of them instead of the
61
- # other. Which leg ran used to be decided by whether this repository held a
62
- # credential, and that question no longer exists; what is left is a leg that
63
- # proves the tree against the contract and a leg that proves the reduced path
64
- # still renders honestly on a machine that has nothing, which is the machine
65
- # article 16 is about. A reduced run proves nothing about the contract and
66
- # nothing about the installed dependency closure of articles 13 and 14, and
67
- # today nothing about this client against a daemon either — the job lists what
68
- # it did not run, so that sentence is checkable against the run rather than
69
- # trusted from here.
62
+ # EXACTLY ONE OF THE TWO LEGS RUNS, AND ONE JOB DECIDES WHICH. The first job
63
+ # below asks the world a single question — can this runner read the control
64
+ # plane's public repository at the tag this client pins — and the two legs carry
65
+ # the two answers as their conditions. It is asked of the remote, with no
66
+ # credential and no checkout, because it is the same question the gate will meet
67
+ # a minute later: a runner that cannot read the tag cannot build the contract,
68
+ # and a runner that can has no business running a leg whose entire claim is that
69
+ # the contract is missing.
70
+ #
71
+ # Running both instead is not a stricter gate, and this file tried it for
72
+ # exactly one run. The reduced leg went red on a runner that could read the tag
73
+ # perfectly well: its step requires the gate to come back reduced, the gate had
74
+ # every reason to come back full, and « the contract is absent » was false on
75
+ # that machine. A leg whose premise does not hold is a check of a machine that
76
+ # is not this one.
77
+ #
78
+ # WHAT THEN PROVES THE REDUCED PATH, since a run of this repository can reach
79
+ # the tag and will therefore always take the full leg. The full leg proves it:
80
+ # `tests/test_contract_absence.py` runs this whole suite again in a child
81
+ # interpreter with the contract's packages made unimportable, and requires that
82
+ # run to come back green with every check it could not run named and counted.
83
+ # That guard is why the reduced path cannot rot unobserved while the leg that
84
+ # renders it sits skipped — which is how the fifteen tests that ask for the
85
+ # contract only when they run came to be discovered by a public runner rather
86
+ # than by this repository.
87
+ #
88
+ # A reduced run proves nothing about the contract and nothing about the
89
+ # installed dependency closure of articles 13 and 14, and today nothing about
90
+ # this client against a daemon either — the job lists what it did not run, so
91
+ # that sentence is checkable against the run rather than trusted from here.
70
92
  name: gate
71
93
 
72
94
  on:
@@ -77,28 +99,38 @@ permissions:
77
99
  contents: read
78
100
 
79
101
  jobs:
80
- full:
81
- name: gate (full)
102
+ # Asked once, before either leg, and answered about the world rather than
103
+ # about this repository: the tag is derived from the pins, the remote is asked
104
+ # whether it has that tag, and the answer is written out for the two legs to
105
+ # read. No credential, and no clone — `git ls-remote` reads refs and nothing
106
+ # else — so a fork gets a truthful answer about its own machine, which is the
107
+ # only kind of answer article 16 can be held to.
108
+ #
109
+ # Why the question is the tag and not the network: a runner that can reach
110
+ # github.com and finds no such tag is in exactly the position of one that can
111
+ # reach nothing at all — there is no source to build the contract from — and
112
+ # the gate's reduced outcome is what honestly describes both.
113
+ reachability:
114
+ name: is the contract reachable
82
115
  runs-on: ubuntu-latest
116
+ outputs:
117
+ contract: ${{ steps.reachable.outputs.contract }}
118
+ tag: ${{ steps.contract.outputs.tag }}
83
119
  steps:
84
120
  - uses: actions/checkout@v5
85
121
  with:
86
122
  path: sayfirst-cli
87
123
 
88
- - uses: astral-sh/setup-uv@v5
89
- with:
90
- version: "0.12.5"
91
-
92
- # Read out of the project file, never spelled here: `scripts/gate.sh`
93
- # reads the same pin out of the same file, and `release.yml` derives the
94
- # same tag with this same script — held as one text by
124
+ # Read out of the project file, never spelled here: the gate reads the
125
+ # same pin out of the same file, and `release.yml` derives the
126
+ # same tag with this same shell — held as one text by
95
127
  # `tests/test_gate_modes.py`, because two readings of one rule is how two
96
128
  # files stop agreeing about it. Shell rather than an interpreter, so that
97
- # the only program of this repository this job runs is the gate.
129
+ # this job runs no program of this repository at all.
98
130
  #
99
131
  # THE ASSUMPTION THIS DERIVATION RESTS ON, stated rather than left to be
100
- # discovered. One tag of the control plane is checked out here, and this
101
- # client pins TWO of the distributions that tag builds. The tag is
132
+ # discovered. One tag of the control plane is what the full leg reads, and
133
+ # this client pins TWO of the distributions that tag builds. The tag is
102
134
  # derived from the `sayfirst-contract` pin because the control plane moves
103
135
  # every distribution it publishes to one version together — its own rule,
104
136
  # held by its own guard, and by nothing in this repository. So the step
@@ -126,10 +158,69 @@ jobs:
126
158
  echo "tag=v${pinned}" >> "$GITHUB_OUTPUT"
127
159
  echo "The contract is read at v${pinned}." >> "$GITHUB_STEP_SUMMARY"
128
160
 
161
+ # The two words this whole file routes on. `--exit-code` is what makes a
162
+ # tag that is not there a status rather than empty output silently read as
163
+ # success, and the tag is passed in as an environment variable rather than
164
+ # interpolated into the shell, which is this repository's rule for every
165
+ # value a runner computes.
166
+ - name: Whether that tag can be read with nothing but this repository
167
+ id: reachable
168
+ env:
169
+ tag: ${{ steps.contract.outputs.tag }}
170
+ run: |
171
+ if git ls-remote --exit-code --tags \
172
+ https://github.com/fredaime/sayfirst-control-plane.git "refs/tags/${tag}" \
173
+ >/dev/null 2>&1; then
174
+ echo "contract=reachable" >> "$GITHUB_OUTPUT"
175
+ {
176
+ echo "### the contract is reachable at ${tag}"
177
+ echo ""
178
+ echo "The full gate runs, and the reduced leg does not: on this runner the"
179
+ echo "contract is not absent, so a leg whose name says it is would be reporting"
180
+ echo "about a machine other than this one."
181
+ } >> "$GITHUB_STEP_SUMMARY"
182
+ else
183
+ echo "contract=absent" >> "$GITHUB_OUTPUT"
184
+ {
185
+ echo "### the contract cannot be read at ${tag}"
186
+ echo ""
187
+ echo "Nothing here holds a credential, so this is a fact about the runner and"
188
+ echo "the tag, not about permission: either the tag is not published yet or"
189
+ echo "this machine cannot reach the repository that carries it. The reduced"
190
+ echo "leg runs instead of the full one and says, in its own name, what it did"
191
+ echo "not prove."
192
+ } >> "$GITHUB_STEP_SUMMARY"
193
+ fi
194
+
195
+ full:
196
+ name: gate (full, python ${{ matrix.python }})
197
+ needs: reachability
198
+ if: needs.reachability.outputs.contract == 'reachable'
199
+ runs-on: ubuntu-latest
200
+ # Every interpreter `requires-python` admits. Python 3.14 changed what
201
+ # `Path.exists` answers and gave `json` a `__main__`, and a gate that ran one
202
+ # interpreter found neither: a range this client installs on is a range
203
+ # this gate runs.
204
+ strategy:
205
+ fail-fast: false
206
+ matrix:
207
+ python: ["3.12", "3.13", "3.14"]
208
+ steps:
209
+ - uses: actions/checkout@v5
210
+ with:
211
+ path: sayfirst-cli
212
+
213
+ - uses: astral-sh/setup-uv@v5
214
+ with:
215
+ version: "0.12.5"
216
+
217
+ # The tag is the one the job above derived, read as its output rather than
218
+ # derived a second time here: this job runs because that job answered, and
219
+ # a second derivation could answer differently.
129
220
  - uses: actions/checkout@v5
130
221
  with:
131
222
  repository: fredaime/sayfirst-control-plane
132
- ref: ${{ steps.contract.outputs.tag }}
223
+ ref: ${{ needs.reachability.outputs.tag }}
133
224
  path: sayfirst-control-plane
134
225
  fetch-depth: 0
135
226
 
@@ -137,13 +228,14 @@ jobs:
137
228
  working-directory: sayfirst-cli
138
229
  env:
139
230
  SAYFIRST_CONTRACT_SOURCE: ${{ github.workspace }}/sayfirst-control-plane
140
- SAYFIRST_CONTRACT_REF: ${{ steps.contract.outputs.tag }}
231
+ SAYFIRST_CONTRACT_REF: ${{ needs.reachability.outputs.tag }}
232
+ SAYFIRST_PYTHON: ${{ matrix.python }}
141
233
  run: |
142
234
  status=0
143
235
  ./scripts/gate.sh || status=$?
144
236
  case "$status" in
145
237
  0)
146
- echo "### gate: full green" >> "$GITHUB_STEP_SUMMARY"
238
+ echo "### gate: full green (python ${{ matrix.python }})" >> "$GITHUB_STEP_SUMMARY"
147
239
  echo "" >> "$GITHUB_STEP_SUMMARY"
148
240
  echo "Format, lint, every test and the dependency-closure guard of articles 13" >> "$GITHUB_STEP_SUMMARY"
149
241
  echo "and 14 ran against a contract built from the control plane's public" >> "$GITHUB_STEP_SUMMARY"
@@ -158,8 +250,15 @@ jobs:
158
250
  ;;
159
251
  esac
160
252
 
253
+ # The other answer. This leg runs when the tag could not be read, which is
254
+ # the only condition under which its name is true — and skipping it otherwise
255
+ # is not a gap: the full leg re-runs this whole suite with the contract's
256
+ # packages hidden, so the reduced path is proven on every run of this
257
+ # repository even though this job is where it is rendered.
161
258
  reduced:
162
259
  name: gate (reduced — contract absent, some checks not run)
260
+ needs: reachability
261
+ if: needs.reachability.outputs.contract == 'absent'
163
262
  runs-on: ubuntu-latest
164
263
  steps:
165
264
  - uses: actions/checkout@v5
@@ -0,0 +1,165 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ # Changelog
3
+
4
+ Every release of this client carries a section here, named for the version the project file
5
+ declares; a release with no section fails the gate.
6
+
7
+ ## Unreleased
8
+
9
+ ## 0.3.0
10
+
11
+ - **`--socket` is optional for a per-user profile**, on every verb that opens a connection. Given
12
+ none, the client looks at the per-user default address — the contract's own rule, the one a
13
+ per-user daemon given no address binds by — and at nothing else: nothing is searched for, and
14
+ whoever answers is still verified to be the expected account's process before a byte is sent. A
15
+ `--socket` somebody typed is the address. A system profile is never given one and still names
16
+ it (`64` otherwise). **What changes for a caller:** an invocation with no `--socket` used to end
17
+ as a usage error (`2`); it now asks, and with nobody at the default address it answers « could
18
+ not ask » (`4`), naming the address it looked at. `evidence audit` with neither `--file` nor
19
+ `--socket` is an online audit at that address rather than a usage error. Needs the contract
20
+ distribution that publishes the rule, which is the one after 0.2.0.
21
+ - **`--pack` takes the name of a pack this distribution ships** (`--pack subprocess`), as well as
22
+ a directory. The spelling alone decides: a designation with a path separator in it, or `.` or
23
+ `..`, is a directory; a bare word is a shipped pack's name and is never read as a directory of
24
+ the working directory. **What changes for a caller:** a pack directory designated by a bare
25
+ relative word (`--pack own-pack`) is now refused (`64`) with the spelling that names it
26
+ (`--pack ./own-pack`). Absolute paths and paths with a separator are read exactly as before.
27
+ `packs check` takes the same two spellings. `docs/PACKS.md` says why this is a designation and
28
+ not a registry.
29
+ - **A target whose first word is `python`, `python3` or `python3.N` is run by that interpreter.**
30
+ The whole command is handed to it, the boundary is installed in that process, and the client,
31
+ the boundary and the contract are lent to it by name — three packages and their metadata, and
32
+ nothing else of this environment. A program in a project's own environment therefore keeps its
33
+ dependencies when this client is installed as a tool. The interpreter has to be Python 3.12 or
34
+ later, and interpreter options are not carried. Before this, such a target was refused (`64`) as
35
+ a script that does not exist; a target with no interpreter word runs as it always did.
36
+ - **An executable script hands over by its shebang.** An agent started as a console script
37
+ (`-- ./myagent`) rather than as `python …` is run by the interpreter its shebang names —
38
+ including `#!/usr/bin/env python3` — so a console-script agent in a project's environment keeps
39
+ its dependencies too. A shebang naming this same interpreter, and a non-executable file, are
40
+ unchanged. **What changes for a caller:** an executable whose shebang names another Python used
41
+ to run in this command's interpreter and fail to import the project's packages; it now runs in
42
+ the interpreter the shebang names.
43
+ - **A runner in the first position is refused** (`64`): `-- uv run app.py`, `-- poetry run app.py`
44
+ and their kind start an interpreter the boundary is not in, so handing the runner over would
45
+ govern nothing. The refusal names the two spellings that work — name the interpreter, or ask the
46
+ runner for it once. Before, `uv` was reported as a script that does not exist.
47
+ - **`instrument verify` matches a run's records by a correlation it stamps, not by their
48
+ connection.** A program that makes two effects of different kinds is two asks, and the shipped
49
+ boundary holds one connection per grant, so its records span two connections. They are still one
50
+ run's, told so by a per-run token the verifier stamps on every ask and the plane records on the
51
+ effect (`correlation_source: boundary_supplied`). **The defect this fixes:** such a run reported
52
+ the second effect `unjudged` and exited `7`; it now verifies clean. The token also tells a
53
+ concurrent same-account run's records apart, which the connection check could not, and it holds
54
+ the first record as well as the rest. Needs the boundary distribution that stamps it, the one
55
+ after 0.2.0.
56
+ - **The `unjudged:` line names the reason the run actually recorded**, one per distinct cause,
57
+ rather than always saying an effect named a start file. A count made of a damaged range, an
58
+ interrupted walk, another execution's record or an uninterposed path now reads as what it was.
59
+ - `docs/PACKS.md` records, with the evidence, why no convenience pack for `requests`, `httpx`,
60
+ `os.system` or a Postgres driver is shipped — a limit of the verifier, not of the classification.
61
+ The verifier confirms an effect from a distinct CPython audit event and shares no state with the
62
+ engine; `requests`/`httpx`/Postgres emit nothing distinct from `socket.connect`, and `os.system`'s
63
+ module is `os`, which the engine imports throughout, so a pack for it would make the engine-
64
+ agnosticism guard read every `import os` as a special case. Each could be governed by hand against
65
+ `sayfirst-boundary`; none can be shipped as a verifiable pack without weakening a guard.
66
+ - **`instrument run --follow-children`** installs the boundary in the Python children the program
67
+ spawns with its environment, before the child's code runs, so a program that starts workers or
68
+ tools of its own has their effects governed too — and a grandchild started the same way. It works
69
+ by a `sitecustomize` on the child's import path (`src/sayfirst_cli/instrument/follow.py`), needs
70
+ this client installed in the interpreter the children run, and fails a child **closed** once the
71
+ bootstrap runs: a child that cannot install the boundary raises before its program runs, and a
72
+ child whose daemon is unreachable fails at the ask exactly as the parent does. A child the
73
+ bootstrap never reaches is not followed and runs as it would without the flag: one started with
74
+ `-S`, `-I` or `-E`, or given a `PYTHONPATH` of its own. It is `run`'s flag, not `verify`'s: a spawned
75
+ child is a separate process the single-process proof cannot see (`harness.py` counts such a child
76
+ as coverage it could not judge), so following would govern what the proof misses.
77
+ - With no `--socket` and nothing at the default address, `instrument run` says which address it
78
+ looked at before the program starts. Nothing else about that run changes: a program that asks
79
+ nothing still runs, and an effect a pack names still fails closed, as the program's own
80
+ exception.
81
+ - The quickstart is three commands, and the page is a transcript of them.
82
+ - **`instrument run` ends with the outcome's own status when the program does not handle it.** A
83
+ boundary outcome the program lets escape — denied, suspended, refused, could not ask — used to
84
+ end the run as any uncaught exception does, with the interpreter's `1`, which is this client's
85
+ « deny » for all four. The traceback is still the program's; the status is now `1`, `5`, `3` or
86
+ `4`. A program that catches the outcome and exits on its own still ends with its own status.
87
+ - **`instrument verify` asks for every effect.** The boundary in front of a verified program holds
88
+ no grant, so an identical effect repeated while a grant lived — answered with nothing asked and
89
+ nothing recorded — is no longer reported `ungoverned` and stopped (`6`). `instrument run` still
90
+ holds its grants (article 10).
91
+ - **`instrument verify` counts one spawn once.** `subprocess.Popen` creates its process through
92
+ `os.posix_spawn` or, from Python 3.14, `_posixsubprocess.fork_exec`, both paths the subprocess
93
+ pack names as not interposed, so one governed spawn was reported with an unjudged effect and
94
+ `7`. A point may now name, among its `uninterposed_events`, the `inner_events` its own call
95
+ raises; such an event is paired with the call it judged when it is that thread's very next event
96
+ and carries the same argument vector, and is counted otherwise. The subprocess pack also names
97
+ `_posixsubprocess.fork_exec`, which is how `multiprocessing` starts a process by default on Linux
98
+ from 3.14, so such a start is counted instead of passing unseen.
99
+ - **`instrument verify` answers `3` for a chain read the plane refused**, as every other command
100
+ does, rather than `4`; and `7` rather than `4` when it never saw the program's own code start,
101
+ since the plane had been asked. `docs/PACKS.md` lists the statuses.
102
+ - **Every connection is bounded.** `ask`, every read and the verifier's chain walk wait at most
103
+ five seconds for an answer; a far end that accepts and never answers is « could not ask » (`4`)
104
+ instead of a command that hangs. A named `--socket` is made absolute once, so a program that
105
+ changes directory asks the daemon it was pointed at (with the contract distribution of this
106
+ release).
107
+ - **Smaller corrections.** A uid with no account name is spelled `user:<uid>`, as the daemon names
108
+ it, instead of ending `instrument run` in a traceback; an `ask` answer nested past the reads'
109
+ depth bound is an answer this client could not read (`4`), not a traceback; `evidence export`
110
+ that cannot write its bundle says « could not save » (`7`); on Python 3.14, an `--out` path this
111
+ client cannot look at is still refused (`64`) before anything is asked, and an entry
112
+ `evidence exports` cannot look at is still « could not check » (`7`); `approvals` names the
113
+ person who acted; `packs check` refuses a pack whose `uninterposed_events` or `inner_events` the
114
+ verifier would refuse.
115
+ - **Interpreter hand-over.** A script whose shebang names a Python that is not there is refused
116
+ (`64`) instead of being run by this client's own interpreter; an interpreter newer than the lent
117
+ packages declare (3.15 and later) is refused; `--follow-children` under a named interpreter that
118
+ does not have this client installed is refused, where every Python child used to die at
119
+ start-up; and under `PYTHONSAFEPATH` the head of the import path is left as the interpreter gave
120
+ it, where the launcher used to replace an entry of the person's own.
121
+ - The full gate runs on Python 3.12, 3.13 and 3.14, the interpreters `requires-python` admits.
122
+
123
+ - Every commit of a change is read for its sign-off on every pull request, and one that carries
124
+ none is refused by name. A range that cannot be read is refused too, rather than reported as
125
+ passing.
126
+ - The publication checklist carries every act the constitution names, in the order they are
127
+ performed, and a guard fails when one of them stops being a line of it. The security policy says
128
+ which of article 0's two paths this repository is published by: a fresh repository, never a
129
+ change of visibility.
130
+ - The gate reduces on a machine with no contract instead of failing there. A test that asks for a
131
+ command only when it runs — this client imports a verb when it dispatches it — is now stood
132
+ down by name and counted, the way a module that could not be imported already was, and a run
133
+ with the contract hidden is required to come back green rather than merely to collect.
134
+ - One of the two gate legs runs, decided by whether the control plane's public repository can be
135
+ read at the tag this client pins. The question is asked once, of the remote, with no credential;
136
+ a leg reporting that the contract is absent no longer runs on a machine where it is not.
137
+
138
+ ## 0.2.0
139
+
140
+ - The evidence surface reads and verifies: `history` pages a scope, `audit` puts the served
141
+ verdict beside a local check and names a finding wherever the two disagree, `export` saves a
142
+ bundle a third party verifies offline with the contract distribution alone, and `exports`
143
+ checks a directory of saved bundles without opening a socket.
144
+ - `instrument run` puts the boundary in front of somebody else's program, reversibly and writing
145
+ nothing; `instrument verify` runs it again under the interpreter's own audit hook and proves,
146
+ from that and the scope's evidence chain alone, that every effect of a named kind was preceded
147
+ by a decision; `instrument apply` is reserved and refuses, saying so.
148
+ - `trace` reads back the record of one decision and follows it into the evidence that holds it:
149
+ the record itself, and where it sits in the chain, bounded to the pages the read walks. Like
150
+ every read from a daemon it names its scope explicitly, and it exits `0` on a read whatever the
151
+ decision it read said.
152
+ - `explain` renders the reason the control plane gave for a decision — the rule it applied and
153
+ the policy version it ran under — in the plane's own words, and composes none of its own. A
154
+ client explains and invokes control semantics; it never derives them, and a command that
155
+ reasoned here would be a second control plane with no evidence behind it.
156
+ - `packs list` prints the convenience packs this distribution ships, one line each with the path
157
+ `--pack` accepts, and `packs check` reads one the way the engine will.
158
+ - `approvals` shows a suspended ask and ends the wait, so the next ask runs the body once.
159
+ - A verdict the verifier could not reach is rendered as it was given rather than as a negative
160
+ fact about the chain.
161
+ - The release itself: this distribution moves to `0.2.0`, and installing it pins
162
+ `sayfirst-contract==0.2.0` and `sayfirst-boundary==0.2.0` — the contract this client speaks and
163
+ the runtime a governed program holds its grant in — at that version and no other. An
164
+ installation of this version therefore carries exactly those two, and upgrading it moves all
165
+ three together.