Skip to content

Commit da4f2d7

Browse files
authored
Merge branch 'main' into dependabot/github_actions/actions/setup-python-6
2 parents 05b5c28 + 315cb32 commit da4f2d7

11 files changed

Lines changed: 225 additions & 40 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
python-version: ${{ matrix.python-version }}
2626

2727
- name: Install package
28-
run: pip install -e ".[blob]"
28+
run: pip install -e "."
2929

3030
- name: Run test suite
3131
run: python3 -m pytest tests/ -q --tb=short

.github/workflows/codeql.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
uses: actions/checkout@v4
3434

3535
- name: Initialize CodeQL
36-
uses: github/codeql-action/init@v3
36+
uses: github/codeql-action/init@v4
3737
with:
3838
languages: ${{ matrix.language }}
3939
build-mode: ${{ matrix.build-mode }}
@@ -42,6 +42,6 @@ jobs:
4242
queries: security-extended
4343

4444
- name: Perform CodeQL Analysis
45-
uses: github/codeql-action/analyze@v3
45+
uses: github/codeql-action/analyze@v4
4646
with:
4747
category: "/language:${{ matrix.language }}"

.kiro/specs/v4-language/tasks.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@
4444
## REQ-V4-3: Public registry
4545

4646
- [x] **V4-3-1** Write `docs/REGISTRY.md`
47-
- [ ] **V4-3-2** Publish the five canonical pipeline symbols to codifide.com (blob write API pending)
47+
- [x] **V4-3-2** Publish the five canonical pipeline symbols to codifide.com ✓ all 5 live
4848
- [x] **V4-3-3** Add `registry` field to capability manifest — deferred (no field defined yet)
4949
- [x] **V4-3-4** Add cookbook entry for publish-and-resolve workflow
50-
- [ ] **V4-3-5** Verify `run --registry https://codifide.com` resolves pipeline symbols
50+
- [x] **V4-3-5** Verify `run --registry https://codifide.com` resolves pipeline symbols ✓ confirmed
5151
- [x] **V4-3-6** File Quill/Glyph dispatch
5252
- [x] **V4-3-7** Registry browser at codifide.com/registry
5353

codifide/runtime/interpreter.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -588,9 +588,12 @@ def _call_primitive(self, name: str, arg_exprs: tuple, frame: _Frame) -> Any:
588588
# an argument. A program that wants to compute over a possibly-refused
589589
# value must handle it explicitly in a `believe` arm. Without this
590590
# check, arithmetic on ⊥ surfaces as a host TypeError.
591-
for a in args:
592-
if isinstance(a, _BottomType):
593-
raise BottomPropagationError(fn=name)
591+
# Exception: is_bottom is the one primitive whose purpose is to
592+
# inspect a bottom value — it must not propagate.
593+
if name != "is_bottom":
594+
for a in args:
595+
if isinstance(a, _BottomType):
596+
raise BottomPropagationError(fn=name)
594597
try:
595598
result = spec.fn(*args)
596599
except CodifideError:

codifide/store/blob_store.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
This mirrors the filesystem store's sharded layout, making the two backends
2222
interchangeable and the blob store browseable.
2323
24-
**Public access.** Symbol blobs are stored with ``access='public'``. The
24+
**Public access.** Symbol blobs are stored with ``access='private'``. The
2525
content is content-addressed — the hash is the identity, so there is no
26-
secret in the content. Public access means agents can resolve symbols
27-
directly from the CDN URL without going through the serverless function,
28-
which is faster and cheaper.
26+
secret in the content. Private access means reads go through the Vercel
27+
Blob API (authenticated with ``BLOB_READ_WRITE_TOKEN``) rather than a
28+
public CDN URL. The serverless GET handler fetches and proxies the bytes.
2929
3030
**Write protection.** The ``put`` method requires ``BLOB_READ_WRITE_TOKEN``.
3131
The serverless function that exposes ``POST /symbols`` should be
@@ -109,7 +109,6 @@ def put(self, name: str, definition: Definition) -> str:
109109
pathname,
110110
data,
111111
options={
112-
"access": "public",
113112
"token": self._token,
114113
"contentType": "application/cbor",
115114
"cacheControlMaxAge": 31536000, # 1 year — immutable
@@ -166,7 +165,7 @@ def get_bytes(self, identity: str) -> bytes:
166165
vb = _require_vercel_blob()
167166
pathname = self._pathname(identity)
168167

169-
# Public blobs have a stable CDN URL. Fetch via the download URL.
168+
# Private blobs require a signed download URL obtained via the API.
170169
try:
171170
result = vb.head(
172171
pathname,
@@ -181,11 +180,14 @@ def get_bytes(self, identity: str) -> bytes:
181180
if not download_url:
182181
raise StoreError(f"Vercel Blob returned no URL for {identity}")
183182

184-
# Fetch the bytes directly from the CDN URL.
183+
# Fetch the bytes using the signed URL (works for both public and private blobs).
185184
import urllib.request
186185
import urllib.error
187186
try:
188-
with urllib.request.urlopen(download_url, timeout=30) as resp:
187+
req = urllib.request.Request(download_url)
188+
if self._token:
189+
req.add_header("Authorization", f"Bearer {self._token}")
190+
with urllib.request.urlopen(req, timeout=30) as resp:
189191
data = resp.read(_MAX_BLOB_BYTES + 1)
190192
except urllib.error.HTTPError as exc:
191193
if exc.code == 404:

crates/codifide-canonical/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ path = "src/bin/codifide_canonical.rs"
1515

1616
[dependencies]
1717
serde_json = "1"
18-
sha2 = "0.10"
18+
sha2 = "0.11"

crates/codifide-interpreter/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ serde_json = "1"
1919
rayon = "1.10"
2020

2121
[dev-dependencies]
22-
criterion = { version = "0.5", features = ["html_reports"] }
22+
criterion = { version = "0.8", features = ["html_reports"] }
2323

2424
[[bench]]
2525
name = "interpreter"

docs/AGENT_QUICKREF.md

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -324,42 +324,34 @@ cand
324324
Both patterns are valid. `cand` + `when` is preferred for three or more
325325
branches; `if/then/else` is preferred for binary choices inside a body.
326326

327-
**`is_bottom` — value inspector, not propagation catcher.**
327+
**`is_bottom` — value inspector.**
328328

329-
`is_bottom(x)` returns `true` when `x` is a literal `bottom` value.
330-
It **cannot** catch a `bottom` that propagated through a bind:
329+
`is_bottom(x)` returns `true` when `x` is a `bottom` value (including `bottom "reason"`).
330+
Both the direct-call and bind patterns work:
331331

332332
```codifide
333-
# WRONGbottom propagates through the bind before is_bottom sees it
333+
# Direct-callis_bottom sees the value before any bind
334334
cand
335-
r <- function_that_refuses()
336-
if is_bottom(r) then "caught" else r # raises BottomPropagationError
335+
if is_bottom(function_that_refuses()) then "refused" else "ok"
337336
338-
# RIGHT — use a believe arm to handle propagated bottom
337+
# Bind pattern — also works; is_bottom receives the bottom value
339338
cand
340339
r <- function_that_refuses()
341-
believe r
342-
ge(conf(r), 0.70) => r
343-
else => bottom # or handle the refusal here
340+
if is_bottom(r) then "refused" else r
344341
```
345342

346-
`is_bottom` is useful when `bottom` is passed as an explicit argument
347-
or stored in a data structure, not when it arrives via function return.
348-
349-
**Direct-call `is_bottom` works.** If you need to check whether a function
350-
refuses *before* binding its result, call `is_bottom` directly on the call
351-
expression — no bind needed:
343+
For conditional routing on a possibly-refused value, `believe` is usually
344+
cleaner and more idiomatic:
352345

353346
```codifide
354-
# Worksis_bottom sees the value before any bind propagates it
347+
# Idiomaticbelieve handles refusal explicitly
355348
cand
356-
if is_bottom(moderate(message)) then "refused" else moderate(message)
349+
r <- function_that_refuses()
350+
believe r
351+
ge(conf(r), 0.70) => r
352+
else => bottom
357353
```
358354

359-
This short-circuits: if `moderate` refuses, the `else` branch never runs.
360-
Note that `moderate` is called twice — once for the check and once for the
361-
value. For expensive functions, prefer the `believe` pattern instead.
362-
363355
## Content-addressed imports
364356

365357
Individual symbol imports bring one symbol into scope by name:

docs/REGISTRY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ published at the following identities:
1818
| `classify_content` | `sha256:377099c5bddb8cebe9e8bc6b8499bb00ea99083798d1b064799ac82c55636fae` |
1919
| `moderate` | `sha256:1bbe69ba7dae84a1fc1a5b335ac2fd9f4be3e4462857db3cc0d38c4af5be4a2a` |
2020
| `route_message` | `sha256:68c15e1108ac195e211634d2755f58353422db61b077690ec59686ad87d2d964` |
21+
| `run_pipeline` | `sha256:9315527239ae775f5f61caedd7f589b2d72e621c384511a1663856235d1a764c` |
22+
| `composed_pipeline` | `sha256:311ba990e2555bba609c230d28072d290bdf1d88b0e1e2d13db553bf34472346` |
2123

2224
These are the symbols from the content-moderation pipeline task spec
2325
(`docs/AGENT_TASK_SPEC.md`). They are the canonical test case for

tests/test_runtime.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,5 +318,156 @@ def inner
318318
self.assertIn("called from", msg)
319319

320320

321+
# ---------------------------------------------------------------------------
322+
# is_bottom — direct-call pattern and propagation footgun regression
323+
# (Sable audit note: AUD-OVERNIGHT-02 follow-up)
324+
# ---------------------------------------------------------------------------
325+
326+
class IsBottomTests(unittest.TestCase):
327+
"""Tests for the is_bottom(f()) direct-call pattern.
328+
329+
The quickref documents two idioms:
330+
- WRONG: r <- f(); is_bottom(r) — bottom propagates through bind
331+
- RIGHT: is_bottom(f()) — is_bottom sees the value before bind
332+
333+
These tests confirm both behaviours and cover BottomWithReason.
334+
"""
335+
336+
def test_is_bottom_on_literal_bottom_returns_true(self) -> None:
337+
src = """
338+
def main
339+
intent "is_bottom on literal bottom"
340+
sig () -> Bool
341+
effects {}
342+
cand
343+
is_bottom(bottom)
344+
"""
345+
self.assertTrue(run(parse(src), "main"))
346+
347+
def test_is_bottom_on_normal_value_returns_false(self) -> None:
348+
src = """
349+
def main
350+
intent "is_bottom on a normal value"
351+
sig () -> Bool
352+
effects {}
353+
cand
354+
is_bottom(42)
355+
"""
356+
self.assertFalse(run(parse(src), "main"))
357+
358+
def test_is_bottom_direct_call_on_refusing_function(self) -> None:
359+
# RIGHT pattern: is_bottom(f()) — sees the bottom before any bind.
360+
src = """
361+
def refuses
362+
intent "always refuses"
363+
sig () -> Any
364+
effects {}
365+
cand
366+
bottom
367+
368+
def main
369+
intent "direct-call is_bottom on a refusing function"
370+
sig () -> Bool
371+
effects {}
372+
cand
373+
is_bottom(refuses())
374+
"""
375+
self.assertTrue(run(parse(src), "main"))
376+
377+
def test_is_bottom_direct_call_on_non_refusing_function(self) -> None:
378+
# RIGHT pattern: is_bottom(f()) where f returns a value.
379+
src = """
380+
def gives_value
381+
intent "returns a string"
382+
sig () -> String
383+
effects {}
384+
cand
385+
"hello"
386+
387+
def main
388+
intent "direct-call is_bottom on a non-refusing function"
389+
sig () -> Bool
390+
effects {}
391+
cand
392+
is_bottom(gives_value())
393+
"""
394+
self.assertFalse(run(parse(src), "main"))
395+
396+
def test_is_bottom_on_bottom_with_reason(self) -> None:
397+
# BottomWithReason is a subclass of _BottomType — is_bottom must catch it.
398+
src = """
399+
def refuses_with_reason
400+
intent "refuses with a reason string"
401+
sig () -> Any
402+
effects {}
403+
cand
404+
bottom "not enough confidence"
405+
406+
def main
407+
intent "is_bottom on bottom-with-reason via direct call"
408+
sig () -> Bool
409+
effects {}
410+
cand
411+
is_bottom(refuses_with_reason())
412+
"""
413+
self.assertTrue(run(parse(src), "main"))
414+
415+
def test_is_bottom_bind_propagation_footgun(self) -> None:
416+
# Previously: r <- f(); is_bottom(r) raised BottomPropagationError
417+
# because is_bottom was treated like any other primitive.
418+
# After the interpreter fix (is_bottom exempt from propagation check),
419+
# this pattern now works correctly — is_bottom returns True.
420+
# The quickref has been updated to reflect this.
421+
src = """
422+
def refuses
423+
intent "always refuses"
424+
sig () -> Any
425+
effects {}
426+
cand
427+
bottom
428+
429+
def main
430+
intent "bind-then-is_bottom now works"
431+
sig () -> Bool
432+
effects {}
433+
cand
434+
r <- refuses()
435+
is_bottom(r)
436+
"""
437+
# Both the direct-call and bind patterns now return True.
438+
self.assertTrue(run(parse(src), "main"))
439+
440+
def test_is_bottom_in_conditional_expression(self) -> None:
441+
# is_bottom used in an inline conditional — common real-world pattern.
442+
src = """
443+
def maybe_refuses
444+
intent "refuses when input is zero"
445+
sig (n: Int) -> Any
446+
effects {}
447+
cand
448+
when gt(n, 0)
449+
n
450+
cand
451+
bottom
452+
453+
def main_refuses
454+
intent "test refusing path"
455+
sig () -> String
456+
effects {}
457+
cand
458+
if is_bottom(maybe_refuses(0)) then "refused" else "ok"
459+
460+
def main_ok
461+
intent "test non-refusing path"
462+
sig () -> String
463+
effects {}
464+
cand
465+
if is_bottom(maybe_refuses(5)) then "refused" else "ok"
466+
"""
467+
m = parse(src)
468+
self.assertEqual(run(m, "main_refuses"), "refused")
469+
self.assertEqual(run(m, "main_ok"), "ok")
470+
471+
321472
if __name__ == "__main__":
322473
unittest.main()

0 commit comments

Comments
 (0)