Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d777d0e
docs: route 2 is the builder; plugin repos marked retired
jothimani-rajendran Oct 21, 2026
8db6a9a
docs: host the built plugin in your own marketplace
jothimani-rajendran Oct 21, 2026
5d2063d
docs: deny dialect of the claude and copilot rows is exit 0 plus JSON
jothimani-rajendran Oct 21, 2026
3235c3e
fix(gate,install): a deny carries the guard's reason; --selection str…
jothimani-rajendran Oct 23, 2026
8aeb861
install: print how to remove a build, per client (IR-12)
jothimani-rajendran Oct 23, 2026
a2708ca
install: --marketplace-name names the build's local marketplace
jothimani-rajendran Oct 23, 2026
51ade7e
docs: name the hosted marketplace with --marketplace-name (IR-13b)
jothimani-rajendran Oct 23, 2026
1e76ed7
chore: chock sync for the deny-message change
jothimani-rajendran Oct 23, 2026
22654d1
IR-8: render the vendored runtime identically on every Python patch l…
jothimani-rajendran Oct 23, 2026
19e13e5
install: keep the marketplace argument optional for existing callers
jothimani-rajendran Oct 23, 2026
fe32a9f
chore: chock sync for the stable runtime render
jothimani-rajendran Oct 23, 2026
56741ea
docs: plugin repos stay as the per-policy route for machines without …
jothimani-rajendran Oct 23, 2026
8adec9a
docs: shorten the README no-chock sentence to the word budget (IR-17)
jothimani-rajendran Oct 23, 2026
310de6e
Merge rp-2a-retire-plugin-repo-links into rp-6-host-your-own-marketplace
claude Oct 23, 2026
4338889
docs: the installed plugin needs only Python and git; the plugin repo…
jothimani-rajendran Oct 23, 2026
b633996
docs: fit agent-plugins.md in the line budget and drop a path the doc…
jothimani-rajendran Oct 23, 2026
b3c86ce
IR-14: every rendered runtime file compiles on Python 3.11
jothimani-rajendran Oct 23, 2026
ccc4dbf
IR-14: format the new test with ruff
jothimani-rajendran Oct 23, 2026
aed8032
docs: unquote the selection code; name the no-chock requirements (IR-F1)
jothimani-rajendran Oct 23, 2026
ad3ec80
Merge rp-2a-retire-plugin-repo-links into rp-6-host-your-own-marketplace
jothimani-rajendran Oct 23, 2026
af196fe
docs: restore the section wrapping, unquote the selection code, name …
jothimani-rajendran Oct 23, 2026
e0e4a9f
install: drop the semicolons from the marketplace-name sentences (IR-…
jothimani-rajendran Oct 23, 2026
f9a76aa
Harden path handling, turn-end reads and person-only checks (SR-3)
jothimani-rajendran Oct 24, 2026
dccabdf
Escape unencodable characters in the stdout wrapper (SR-5)
jothimani-rajendran Oct 24, 2026
975abcc
Tolerate a broken pipe on the escaped retry (SR-5)
jothimani-rajendran Oct 24, 2026
4ef158a
test: keep interpreter start-up out of the early-exit pattern-judge b…
jothimani-rajendran Oct 24, 2026
57ad94b
chore: chock sync for the SR-3 hardening
jothimani-rajendran Oct 24, 2026
1b87cd5
Merge pull request #314 into rc
jothimani-rajendran Oct 25, 2026
b9e6653
Merge pull request #315 into rc
jothimani-rajendran Oct 25, 2026
f70eb2c
Merge pull request #318 into rc
jothimani-rajendran Oct 25, 2026
31e96b1
Merge pull request #319 into rc
jothimani-rajendran Oct 25, 2026
1aa819c
Merge pull request #320 into rc
jothimani-rajendran Oct 25, 2026
16a1cfe
Merge pull request #321 into rc
jothimani-rajendran Oct 25, 2026
caeb16e
Merge pull request #324 into rc
jothimani-rajendran Oct 25, 2026
799b9ca
Merge pull request #325 into rc
jothimani-rajendran Oct 25, 2026
f592114
Merge pull request #326 into rc
jothimani-rajendran Oct 25, 2026
51c0897
rc: chock sync and runtime goldens for rc.2
jothimani-rajendran Oct 25, 2026
6cbe32a
rc: runtime goldens for rc.2
jothimani-rajendran Oct 25, 2026
2ccc76c
Merge origin/main into rc (IR-25)
jothimani-rajendran Oct 26, 2026
0326bef
Merge origin/v1-runtime into rc (IR-25)
jothimani-rajendran Oct 26, 2026
cc695c0
fix: make _in_scope's blind flag keyword-only (FBT001) after the v1-r…
jothimani-rajendran Oct 26, 2026
88a6193
Merge main into rc
jothimani-rajendran Oct 26, 2026
33689b9
rc: sync the compiled runtime and goldens for rc.3
jothimani-rajendran Oct 26, 2026
acc5a1e
rc: sync the compiled runtime and goldens for rc.3
jothimani-rajendran Oct 26, 2026
a3e3efd
test: pin the eval CLI's cp1252 console output (IR-33)
jothimani-rajendran Oct 27, 2026
b79a275
fix(guardrails): report an uncommitted repo-scope off as staying on (…
jothimani-rajendran Oct 27, 2026
86e04ad
Merge main into rc
jothimani-rajendran Oct 28, 2026
f2b5c44
Merge pull request #335 from open-coder-ai/ir-33-eval-cp1252-test
jothimani-rajendran Oct 28, 2026
0233e1b
Merge pull request #337 from open-coder-ai/ir-f42-off-status
jothimani-rajendran Oct 28, 2026
c4e9a4a
rc: sync the compiled runtime, goldens and lockfile (v2) for rc.4
jothimani-rajendran Oct 28, 2026
6503e92
Merge main into rc
jothimani-rajendran Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 136 additions & 54 deletions .chock/bin/_chock_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ def log_settings_read(path):
problems.append(f'{path}: max_bytes must be {_LOG_BYTES_RANGE}, not {size!r}; using the next scope or 1048576')
unknown = sorted(set(doc) - set(LOG_SETTINGS_KEYS))
if unknown:
problems.append(''.join([format(path), ': unknown key(s) ', format(', '.join(map(str, unknown))), '; only level and max_bytes are read']))
names = ', '.join(map(str, unknown))
problems.append(f'{path}: unknown key(s) {names}; only level and max_bytes are read')
return (found, problems)

def log_settings_paths(repo_root):
Expand Down Expand Up @@ -858,7 +859,7 @@ def evaluate(argv: list[str], command: str, tool: str='', *, can_ask: bool | Non
verdict, message = run_guard_detailed(guard, command, tool, can_ask=can_ask)
GUARD_LAST['outcome'] = verdict
if verdict == GUARD_BLOCKED:
return (VERDICT_DENY, f'Blocked by chock policy: {guard.stem}')
return (VERDICT_DENY, f'Blocked by chock policy: {guard.stem}: {message}')
if verdict == GUARD_ASKED:
return (VERDICT_ESCALATE, f'chock policy {guard.stem} asks before this runs: {message}' if message else f'chock policy {guard.stem} asks for confirmation before this runs (guard gave no reason).')
if verdict == GUARD_ERRORED:
Expand Down Expand Up @@ -949,7 +950,8 @@ def dlog_target(event, repo):
tool = str(getattr(event, 'tool', None) or '')
mcp = _DLOG_MCP_TOOL.match(tool)
if mcp:
return log_redact(''.join([format(mcp['server']), '/', format(mcp['tool'])]))[:_DLOG_MCP_CAP]
server, name = (mcp['server'], mcp['tool'])
return log_redact(f'{server}/{name}')[:_DLOG_MCP_CAP]
command, path = (getattr(event, 'command', None), getattr(event, 'path', None))
if command:
return dlog_command_print(command)
Expand Down Expand Up @@ -1075,8 +1077,12 @@ def _dlog_reason(source, outcome):
if source == HALT_REASON:
return HALT_REASON
if source == 'guard' and 'outcome' in GUARD_LAST:
return ''.join(['guard-', format(GUARD_LAST['outcome'])])
return source if source == 'fault' else ''.join([format(source), '-', format(_DLOG_SPOKEN.get(outcome, 'allow'))])
guarded = GUARD_LAST['outcome']
return f'guard-{guarded}'
if source == 'fault':
return source
spoken = _DLOG_SPOKEN.get(outcome, 'allow')
return f'{source}-{spoken}'

def _dlog_paths(route, runtime, agent, event):
"""(log file, repository whose settings apply) for this call."""
Expand Down Expand Up @@ -1149,6 +1155,8 @@ def log_heartbeat(event, manifests):

_CREATE_TEXT = 'file_text'

_CREATE_KEYS = ('content', _CREATE_TEXT)

def _edit_call_input(event):
raw = getattr(event, 'raw', None)
tool_input = raw.get('tool_input') if isinstance(raw, dict) else None
Expand Down Expand Up @@ -1210,12 +1218,16 @@ def edited_text(event, root=None):
return text

def created_text(event):
"""The whole file a write call carries: the adapter's content, else `tool_input.file_text`; None otherwise."""
"""The whole file a write call carries: the adapter's content, else the call's own `content` or `file_text`.

An adapter reads an empty `content` as absent, so the call's own input is read too: a write of
nothing is still a write to its path.
"""
content = getattr(event, 'content', None)
if isinstance(content, str):
return content
text = _edit_call_input(event).get(_CREATE_TEXT)
return text if isinstance(text, str) else None
tool_input = _edit_call_input(event)
return next((text for text in map(tool_input.get, _CREATE_KEYS) if isinstance(text, str)), None)

def added_from_event(event):
"""What an edit call introduces -- what `added_lines` means for it -- or {} for any other call."""
Expand Down Expand Up @@ -1386,7 +1398,8 @@ def _patch_not_empty(state, text):
if last is None or last['op'] != 'update':
return
if not last['body'] and state['mode'] == 'update':
_patch_refuse(state['number'], text, ''.join(['update of ', format(repr(last['path'])), ' has no hunk']))
target = last['path']
_patch_refuse(state['number'], text, f'update of {target!r} has no hunk')
if last['body'] and (not (last['body'][-1]['old'] or last['body'][-1]['new'])):
_patch_refuse(state['number'], text, 'a hunk has no lines')

Expand Down Expand Up @@ -1516,7 +1529,11 @@ def _read(root, path):
return None

def patched_files(event, root=None):
"""{path: text} for every file an apply_patch call adds or updates and can be rebuilt."""
"""{path: text} for every file an apply_patch call adds or updates and can be rebuilt.

A file it deletes, or moves away from, is named with empty text: it has no content left to judge,
but its path is still a write that path-based gates must see.
"""
patch = patch_text(event)
if patch is None:
return {}
Expand All @@ -1528,15 +1545,15 @@ def patched_files(event, root=None):
disk[key] = files[key] = ''.join((line + '\n' for line in body))
elif op == 'delete':
disk[key] = None
files.pop(key, None)
files[key] = ''
else:
before = disk[key] if key in disk else _read(root, key)
after = apply_hunks(before, body) if before is not None else None
if after is not None:
target = _patch_key(event, moved_to) if moved_to else key
if target != key:
disk[key] = None
files.pop(key, None)
files[key] = ''
disk[target] = files[target] = after
return files

Expand Down Expand Up @@ -2168,6 +2185,85 @@ def workspace_root(event):
path = getattr(event, 'path', None)
return next((root for root in roots if path and _holds(root, path)), roots[0] if roots else None)

_CASE_BLIND = sys.platform in ('darwin', 'win32')

def repo_relative(path, root):
"""`path` relative to `root` in POSIX form, as scope globs are written; as given when outside `root`."""
text = str(path)
if root is None:
return text
windows = _chock_os.name == 'nt' or str(root)[1:2] == _DRIVE_COLON
flavour = _chock_PureWindowsPath if windows else _chock_PurePosixPath
try:
return _folded(flavour(str(root)) / text).relative_to(flavour(str(root))).as_posix()
except ValueError:
pass
if windows != (_chock_os.name == 'nt'):
return text
try:
return _chock_Path(root, text).resolve().relative_to(_chock_Path(root).resolve()).as_posix()
except (OSError, ValueError, RuntimeError):
return text

def on_disk_case(name, root):
"""`name` (repo-relative, POSIX) with each existing component spelled as the filesystem stores it."""
here, spelled = (_chock_Path(root), [])
for part in _chock_PurePosixPath(name).parts:
try:
stored = _chock_os.listdir(here)
except OSError:
stored = []
found = part if part in stored else next((s for s in stored if s.casefold() == part.casefold()), part)
spelled.append(found)
here = here / found
return _chock_PurePosixPath(*spelled).as_posix()

def repo_paths(path, root, *, case_blind=_CASE_BLIND):
"""Every repo-relative name a write is judged under: as written, and through symlinks and case."""
lexical = repo_relative(path, root)
if root is None or (str(root)[1:2] == _DRIVE_COLON) != (_chock_os.name == 'nt'):
return (lexical,)
try:
resolved = _chock_Path(root, str(path)).resolve().relative_to(_chock_Path(root).resolve()).as_posix()
except (OSError, ValueError, RuntimeError):
return (lexical,)
names = [lexical, resolved]
if case_blind:
names += [on_disk_case(name, root) for name in (lexical, resolved) if not is_outside(name)]
return tuple(dict.fromkeys(names))

_MODE_LINK = 40960

def _open_text(target, kind):
"""(text, why) of a link's target or a regular file, opened without following a link or waiting on a pipe."""
try:
if kind == _MODE_LINK:
return (_chock_os.readlink(target), None)
if kind != STOP_MODE_FILE:
return (None, f'{target} is neither a file nor a link')
fd = _chock_os.open(target, _chock_os.O_RDONLY | getattr(_chock_os, 'O_NOFOLLOW', 0) | getattr(_chock_os, 'O_NONBLOCK', 0))
with _chock_os.fdopen(fd, 'rb') as handle:
return (handle.read().decode('utf-8'), None)
except UnicodeDecodeError:
return (None, None)
except OSError as exc:
return (None, f'{target} could not be read ({exc})')

def worktree_text(target):
"""(text, why): `target` as the worktree holds it, never opening a FIFO, device or the file a link names.

A file that is gone reads as "" (its path is still judged), a directory or undecodable file as no
text, a link as the target git stores for it. Anything else is refused with a reason: opening a
FIFO or device can block the hook until its client's timeout, which a client may read as an allow.
"""
try:
kind = _chock_os.lstat(target).st_mode & STOP_MODE_MASK
except FileNotFoundError:
return ('', None)
except OSError as exc:
return (None, f'{target} could not be examined ({exc})')
return (None, None) if kind == STOP_MODE_DIR else _open_text(target, kind)

_REPO_STOP_PROJECT_ENV = ('CLAUDE_PROJECT_DIR', 'GEMINI_PROJECT_DIR', 'OPENHANDS_PROJECT_DIR')

_REPO_STOP_PROJECT_KEYS = ('workspacePaths',)
Expand Down Expand Up @@ -2294,18 +2390,29 @@ def repo_stop_refusal(event):

_GIT_DIR = '.git'

def _repository(folder):
def repository_of(folder):
"""The repository `folder` is in: the nearest folder holding `.git`, or None outside one."""
here = _chock_Path(folder).absolute()
if not here.is_dir():
return None
return next((f for f in (here, *here.parents) if (f / _GIT_DIR).exists()), None)

def judged_root(named):
"""The top level of `named`'s repository; `named` as given when relative (a drive-lettered root on POSIX)."""
path = _chock_Path(named)
return repository_of(path) or path if path.is_absolute() else path

def stop_roots(event, fallback):
"""The event's `cwd` and every workspace root inside a repository, the first per repository; else [fallback]."""
"""The top level of each repository the event's `cwd` and workspace roots are in; else [fallback].

The top level, not the named folder: `git status` lists paths relative to it, so a subdirectory
would resolve every listed path against the wrong folder and judge nothing.
"""
by_repository = {}
for named in filter(None, (getattr(event, 'cwd', None), *workspace_roots(event))):
repository = _repository(named)
repository = repository_of(named)
if repository is not None:
by_repository.setdefault(repository, _chock_Path(named))
by_repository.setdefault(repository, judged_root(named))
return list(by_repository.values()) or [fallback]

def per_root(said):
Expand Down Expand Up @@ -2383,37 +2490,8 @@ def writes_from_event(event, root=None):
return {}
return {str(path): content}

def repo_relative(path, root):
"""`path` relative to `root` in POSIX form, as scope globs are written; as given when outside `root`."""
text = str(path)
if root is None:
return text
windows = _chock_os.name == 'nt' or str(root)[1:2] == _DRIVE_COLON
flavour = _chock_PureWindowsPath if windows else _chock_PurePosixPath
try:
return _folded(flavour(str(root)) / text).relative_to(flavour(str(root))).as_posix()
except ValueError:
pass
if windows != (_chock_os.name == 'nt'):
return text
try:
return _chock_Path(root, text).resolve().relative_to(_chock_Path(root).resolve()).as_posix()
except (OSError, ValueError, RuntimeError):
return text

def repo_paths(path, root):
"""Every repo-relative name a write is judged under: as written, and through symlinks and case."""
lexical = repo_relative(path, root)
if root is None or (str(root)[1:2] == _DRIVE_COLON) != (_chock_os.name == 'nt'):
return (lexical,)
try:
resolved = _chock_Path(root, str(path)).resolve().relative_to(_chock_Path(root).resolve()).as_posix()
except (OSError, ValueError, RuntimeError):
return (lexical,)
return tuple(dict.fromkeys((lexical, resolved)))

def changed_paths(repo_root, deadline=None, why=None):
"""Every uncommitted path; None (reason appended to `why`) when git gave no answer, [] only outside a repository."""
"""Every uncommitted path, a deleted file and a rename's old name included; None (reason in `why`) when git gave no answer."""
deadline, why = (engine_deadline() if deadline is None else deadline, [] if why is None else why)
argv = [_GIT, '-C', str(repo_root), 'status', '--porcelain=v1', '--untracked-files=all', '-z']
try:
Expand All @@ -2437,8 +2515,8 @@ def changed_paths(repo_root, deadline=None, why=None):
for field in fields:
status, path = (field[:2], field[3:])
if status.startswith(_RENAMED):
next(fields, None)
if _DELETED not in status and path:
paths.append(next(fields, ''))
if path:
paths.append(path)
return paths

Expand All @@ -2454,10 +2532,14 @@ def writes_from_worktree(repo_root, deadline=None, why=None):
return None
writes = {}
for path in paths:
try:
writes[path] = _chock_Path(repo_root, path).read_text(encoding='utf-8')
except (OSError, UnicodeDecodeError):
continue
text, unsafe = worktree_text(_chock_Path(repo_root, path))
if unsafe:
if why is not None:
why.append(unsafe)
sys.stderr.write(f'chock: {unsafe}, worktree not checked\n')
return None
if text is not None:
writes[path] = text
return writes

def run_gate(gate, writes, event, run_in, deadline):
Expand Down Expand Up @@ -2496,15 +2578,15 @@ def root_for(gate):
return parents[_GATE_DEPTH_TO_CHOCK].parent

def repo_root_for(event, gate):
"""The repository under judgement: the compiled layout's root, else the event's cwd, else its workspace root.
"""The repository under judgement: the compiled layout's root, else the top level of the event's cwd or workspace root.

Of several workspace roots, the one holding the written path; this process's cwd only when there are none.
"""
root = root_for(gate)
if root is not None:
return root
cwd = getattr(event, 'cwd', None) or workspace_root(event)
return _chock_Path(cwd) if cwd else _chock_Path.cwd()
return judged_root(cwd) if cwd else _chock_Path.cwd()

def writes_for(event, gate, deadline=None, why=None, root=None):
"""What this event puts under judgement: the call's own text, or what the turn left behind (None: unlisted)."""
Expand Down
2 changes: 1 addition & 1 deletion .chock/bin/claude_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ def degrade(decision, event):
# Not self-contained: chock's guard logic lives in the sibling _chock_runtime.py, loaded from this
# file's own folder and pinned to the sha256 of that file's bytes (CRLF read as LF), hashed before they are executed.
# Missing, unreadable, another version, or failing to import: every call refuses (_CHOCK_RUNTIME_FAULT), never allows.
_CHOCK_RUNTIME_PIN = '7ad185c06c529b9d8174cc9faeece81f92a75732282baa0bccee772d06d92787'
_CHOCK_RUNTIME_PIN = 'a7302f5f8e1deb74d6f245a97474228ce9258ed2c04313a30af7799787a9d259'


def _chock_load_runtime():
Expand Down
2 changes: 1 addition & 1 deletion .chock/bin/codex_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ def degrade(decision, event):
# Not self-contained: chock's guard logic lives in the sibling _chock_runtime.py, loaded from this
# file's own folder and pinned to the sha256 of that file's bytes (CRLF read as LF), hashed before they are executed.
# Missing, unreadable, another version, or failing to import: every call refuses (_CHOCK_RUNTIME_FAULT), never allows.
_CHOCK_RUNTIME_PIN = '7ad185c06c529b9d8174cc9faeece81f92a75732282baa0bccee772d06d92787'
_CHOCK_RUNTIME_PIN = 'a7302f5f8e1deb74d6f245a97474228ce9258ed2c04313a30af7799787a9d259'


def _chock_load_runtime():
Expand Down
2 changes: 1 addition & 1 deletion .chock/bin/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ def degrade(decision, event):
# Not self-contained: chock's guard logic lives in the sibling _chock_runtime.py, loaded from this
# file's own folder and pinned to the sha256 of that file's bytes (CRLF read as LF), hashed before they are executed.
# Missing, unreadable, another version, or failing to import: every call refuses (_CHOCK_RUNTIME_FAULT), never allows.
_CHOCK_RUNTIME_PIN = '7ad185c06c529b9d8174cc9faeece81f92a75732282baa0bccee772d06d92787'
_CHOCK_RUNTIME_PIN = 'a7302f5f8e1deb74d6f245a97474228ce9258ed2c04313a30af7799787a9d259'


def _chock_load_runtime():
Expand Down
2 changes: 1 addition & 1 deletion .chock/bin/devin.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,7 @@ def degrade(decision, event):
# Not self-contained: chock's guard logic lives in the sibling _chock_runtime.py, loaded from this
# file's own folder and pinned to the sha256 of that file's bytes (CRLF read as LF), hashed before they are executed.
# Missing, unreadable, another version, or failing to import: every call refuses (_CHOCK_RUNTIME_FAULT), never allows.
_CHOCK_RUNTIME_PIN = '7ad185c06c529b9d8174cc9faeece81f92a75732282baa0bccee772d06d92787'
_CHOCK_RUNTIME_PIN = 'a7302f5f8e1deb74d6f245a97474228ce9258ed2c04313a30af7799787a9d259'


def _chock_load_runtime():
Expand Down
Loading
Loading