Skip to content

Commit b778a2b

Browse files
committed
Fix complete handoff permission profile
1 parent 7cf7f5e commit b778a2b

11 files changed

Lines changed: 235 additions & 177 deletions

File tree

.agents/skills/handoff/SKILL.md

Lines changed: 85 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -29,49 +29,64 @@ pair for `$resume-from-handoff`.
2929
amend workarounds.
3030
- Use evidence from the current turn for every status, count, and verification
3131
claim.
32-
- If a command fails, halt the current phase and report the exact failure.
32+
- If a command fails, use only the permission fallback or DNS retry declared
33+
below. If that exact fallback also fails, halt the current phase and report
34+
the exact final failure.
3335

34-
## Git Metadata Capability Gate
36+
## Git And Remote Capability Gate
3537

3638
Before Phase 0, resolve the repository's actual Git metadata directory:
3739

3840
```bash
3941
git rev-parse --absolute-git-dir
4042
```
4143

42-
Run `test -w '<absolute-git-dir>'` against the exact returned path, shell-quoted.
43-
Do not request escalation for this check. The `game_studios` permission profile
44-
must make Git metadata writable. The same profile preserves normal workspace
45-
writes to `.agents/` and `.codex/` so CCGS's own maintenance workflows remain
46-
usable; only the declared secret paths stay denied.
47-
48-
If the lookup fails or the check reports the Git directory is not writable,
49-
halt before Phase 0. Report the exact path and result as a permission-profile
50-
configuration mismatch. Direct the user to install the current CCGS permission
51-
profile and start a new session so that profile becomes active. Do not begin the
52-
review gate, rotate continuity files, run `chmod`, delete lock files, or try an
53-
indirect command workaround. Do not tell the user to switch `/permissions`
54-
modes: approval routing cannot repair a filesystem rule already resolved for the
55-
session.
56-
57-
## Push Approval Capability Gate
58-
59-
Before Phase 0, inspect the active session permission instructions. The active
60-
approval policy must permit sandbox escalation because the `game_studios`
61-
profile intentionally keeps outbound network access restricted until the
62-
authorized handoff push. The installed project config declares
63-
`approval_policy = "on-request"`; a session started before that config became
64-
active can still report `approval_policy = "never"` and explicitly forbid
65-
`sandbox_permissions`.
66-
67-
If the active policy is `never`, the permission instructions forbid
68-
`sandbox_permissions`, or the command tool cannot request escalation, halt
69-
before Phase 0. Report a stale-session approval-policy configuration mismatch
70-
and direct the user to start one new session with the installed project config.
71-
Do not begin the review gate, update continuity files, stage, commit, issue an
72-
un-escalated `git push`, or ask the user to switch `/permissions` modes. A new
73-
session is required only to load the corrected project policy; routine handoffs
74-
must not require manual permission-mode switching.
44+
Run `test -w '<absolute-git-dir>'` against the exact returned path, shell-quoted,
45+
using the user's active session permissions. If it is not writable because of
46+
the sandbox and scoped escalation is available, repeat that exact check once
47+
with `sandbox_permissions` set to `"require_escalated"`. If the active context
48+
already reports the Git directory as read-only, request that scoped escalation
49+
on the first attempt. This capability check must not run `chmod`, create a probe
50+
file, delete lock files, or change command shape.
51+
52+
Also resolve the current branch, configured upstream, and exact push URL before
53+
Phase 0:
54+
55+
```bash
56+
git rev-parse --abbrev-ref HEAD
57+
git rev-parse --abbrev-ref --symbolic-full-name '@{u}'
58+
git remote get-url --push <upstream-remote>
59+
git remote get-url --push origin
60+
```
61+
62+
Treat a non-zero upstream lookup as the expected no-upstream case. Use the
63+
upstream remote when present; otherwise use `origin`. Halt if the required
64+
remote or push URL is missing. Never display embedded credentials; redact them
65+
if the configured URL contains any.
66+
67+
Test the exact destination before review or continuity writes:
68+
69+
```bash
70+
git ls-remote --heads '<verified-push-url>' 'refs/heads/<current-branch>'
71+
```
72+
73+
An exit code of zero with no matching ref is valid for a new remote branch. Use
74+
the user's active session permissions. If the active context explicitly reports
75+
network access as unavailable, request scoped escalation on the first attempt
76+
with `prefix_rule` set to `["git", "ls-remote"]`; otherwise run normally first.
77+
If that normal attempt fails solely because sandboxed network access is
78+
unavailable, repeat the exact command once with `sandbox_permissions` set to
79+
`"require_escalated"`.
80+
If the network-capable attempt returns the exact pre-contact DNS error
81+
`Could not resolve host`, retry that same command once with the same permission
82+
mode. Do not retry authentication, authorization, transport, or other errors.
83+
84+
If the Git metadata check or remote check still fails, halt before Phase 0 and
85+
report the exact path, destination, command result, and permission fallback
86+
attempted. Do not begin the review gate or rotate continuity files. The shipped
87+
CCGS config selects the complete `game_studios` profile but does not override
88+
the user's approval policy. The skill must not instruct the user to switch
89+
`/permissions` modes.
7590

7691
## Phase 0: Review Gate
7792

@@ -259,45 +274,47 @@ If there are no relevant uncommitted changes, skip the commit and say why.
259274

260275
Otherwise stage only the relevant paths by name. Avoid broad staging unless the
261276
user explicitly asked for it. `$handoff` invocation is commit authorization for
262-
the relevant handoff work. Run staging without requesting escalation; the
263-
active `game_studios` profile grants Git metadata writes. If staging cannot
264-
write the Git index, halt Phase 3 and report the exact failure without retrying
265-
through a different command shape.
277+
the relevant handoff work. Run staging using the user's active session
278+
permissions. If it fails solely because the sandbox denies Git metadata writes,
279+
repeat the exact `git add` command once with `sandbox_permissions` set to
280+
`"require_escalated"` and `prefix_rule` set to `["git", "add"]`. Do not broaden
281+
the path set or use a different staging command.
266282

267283
Before committing, verify:
268284

269285
```bash
270286
git diff --cached --name-status
271287
```
272288

273-
Commit with the standard handoff subject without requesting escalation. Include
274-
a Codex co-author trailer only if that is normal for this repo. If the commit
275-
cannot write Git metadata, halt Phase 3 and report the exact failure without
276-
retrying through a different command shape.
289+
Commit with the standard handoff subject using the active session permissions.
290+
If it fails solely because the sandbox denies Git metadata writes, repeat that
291+
exact `git commit` command once with `sandbox_permissions` set to
292+
`"require_escalated"` and `prefix_rule` set to `["git", "commit"]`. Include a
293+
Codex co-author trailer only if that is normal for this repo. Halt on any other
294+
failure or if the exact permission fallback fails.
277295

278296
Never use `--no-verify`. Never amend as a workaround for a failed hook.
279297

280298
## Phase 4: Push Handoff
281299

282-
Determine the branch and configured upstream:
300+
Recheck the branch and configured upstream:
283301

284302
```bash
285303
git rev-parse --abbrev-ref HEAD
286304
git rev-parse --abbrev-ref --symbolic-full-name '@{u}'
287305
```
288306

289307
Treat a non-zero upstream lookup as the expected no-upstream case, not as a
290-
Phase failure. Do not substitute a different branch. If an upstream exists,
291-
derive its remote name from the returned `<remote>/<branch>` value and verify
292-
that remote's push URL. If no upstream exists, verify `origin` because it is the
293-
only remote authorized for the setup push:
308+
Phase failure. Do not substitute a different branch. Verify that the branch,
309+
upstream state, remote, and push URL still match the destination that passed the
310+
preflight. If any changed, halt instead of pushing to an untested destination:
294311

295312
```bash
296313
git remote get-url --push <upstream-remote>
297314
git remote get-url --push origin
298315
```
299316

300-
Run only the command that matches the detected upstream state. Halt if the
317+
Run only the lookup that matches the detected upstream state. Halt if the
301318
required push remote is missing.
302319

303320
Treat the resolved push URL, current branch/upstream, and explicit `$handoff`
@@ -318,29 +335,32 @@ one of these command shapes:
318335
- Existing upstream: `git push`.
319336
- No upstream: `git push -u origin <branch>`.
320337

321-
Request the required escalation with the scoped approval prefix
322-
`["git", "push"]`. The justification must state that this is the explicitly
323-
authorized, non-force handoff push; name the verified push URL; and identify the
324-
current branch/upstream. Do not claim an authenticated account or repository
325-
permission unless it was actually verified. Explicit `$handoff` invocation is
326-
normal push authorization for the standard handoff commit. Never force-push.
327-
328-
The first and only push attempt must call the command tool directly with
329-
`sandbox_permissions` set to `"require_escalated"` and `prefix_rule` set to
330-
`["git", "push"]`. Do not issue `git push` without escalation first and do not
331-
run an un-escalated probe. If the tool surface rejects or omits the requested
332-
escalation, treat that as a policy failure and do not let the command fall back
333-
to the network-restricted sandbox.
338+
Use the user's active session permissions. If the active context explicitly
339+
reports network access as unavailable, request scoped escalation on the first
340+
push attempt with `sandbox_permissions` set to `"require_escalated"` and
341+
`prefix_rule` set to `["git", "push"]`; otherwise run the matching push command
342+
normally first. If that normal attempt fails solely because sandboxed network
343+
access is unavailable, repeat the exact push command once with the same scoped
344+
escalation. The justification must state that this is the explicitly authorized,
345+
non-force handoff push; name the verified push URL; and identify the current
346+
branch/upstream. Do not claim an authenticated account or repository permission
347+
unless it was actually verified. Explicit `$handoff` invocation is normal push
348+
authorization for the standard handoff commit. Never force-push.
349+
350+
If the network-capable push attempt returns the exact pre-contact DNS error
351+
`Could not resolve host`, retry that exact push command once with the same
352+
permission mode. This retry is safe because name resolution failed before the
353+
remote could be contacted. Do not retry authentication, authorization,
354+
transport, hook, or ambiguous failures, and never change the push command.
334355

335356
The actual `git push` is the authoritative network and Git-authentication check.
336357
If it fails, report Git's exact error and do not reinterpret a preceding GitHub
337358
CLI result as proof of the cause.
338359

339360
If policy or automatic approval review rejects the push, halt Phase 4. Report
340361
the exact rejection and do not retry with another command shape, indirect
341-
execution, or workaround. When the current Codex surface supports it, direct
342-
the user to `/approve` and the denied action for the documented single-action
343-
retry; otherwise ask the user to perform the push directly.
362+
execution, or workaround. Do not instruct the user to change the whole session's
363+
permission mode; report the denied scoped action instead.
344364

345365
## Phase 5: Report And Stop
346366

.codex/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ Current package version:
1313

1414
Current status:
1515
- The package version is `v0.6.1`.
16-
- Post-`v0.6.1` hardening keeps Git metadata writable, sets project approval
17-
routing to `on-request`, and requires `$handoff` to use one scoped escalated
18-
push instead of falling into a network-restricted DNS failure.
16+
- Post-`v0.6.1` correction completes the default `game_studios` profile with
17+
Git/runtime writes, `.env*` denials, and exact `github.com` access without a
18+
project approval-policy override. `$handoff` now preflights Git and the exact
19+
remote, uses scoped permission fallback only when needed, and retries once
20+
only for a pre-contact DNS failure.
1921
- `v0.6.1` hardens `$handoff` push routing and fails closed on policy denial
2022
without alternate-command retries.
2123
- `v0.6.0` adds verified CLI/desktop custom-role activation guidance, raw
@@ -47,9 +49,9 @@ Coexistence rules:
4749
- Keep optional `.agents/skills/gen-asset/**` content project-owned; the
4850
installer only makes that subtree trackable and never adds it to the package
4951
manifest.
50-
- The default permission profile denies all access to root and nested `.env*`
51-
files. Non-secret templates that Codex should edit must use a different name,
52-
such as `config.example`.
52+
- The default `game_studios` profile denies all access to root and nested
53+
`.env*` files. Non-secret templates that Codex should edit must use a
54+
different name, such as `config.example`.
5355

5456
Install and release notes:
5557
- Fresh targets receive a full install. Existing targets without ownership state

.codex/config.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
approval_policy = "on-request"
21
default_permissions = "game_studios"
32

43
[permissions.game_studios]
4+
description = "CCGS workspace access with secret-file and GitHub boundaries."
55
extends = ":workspace"
66

77
[permissions.game_studios.filesystem.":workspace_roots"]
@@ -11,5 +11,11 @@ extends = ":workspace"
1111
".env*" = "deny"
1212
"**/.env*" = "deny"
1313

14+
[permissions.game_studios.network]
15+
enabled = true
16+
17+
[permissions.game_studios.network.domains]
18+
"github.com" = "allow"
19+
1420
[tui]
1521
status_line = ["model-with-reasoning", "current-dir", "context-used", "git-branch"]

.codex/lib/validate_rules.py

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717
"git_branch": "git-branch",
1818
}
1919

20+
PROJECT_PERMISSION_KEYS = {
21+
"approval_policy",
22+
"sandbox_mode",
23+
"sandbox_workspace_write",
24+
}
25+
26+
REQUIRED_DEFAULT_PERMISSIONS = "game_studios"
27+
2028
REQUIRED_WORKSPACE_RULES = {
2129
".git": "write",
2230
".agents": "write",
@@ -25,7 +33,9 @@
2533
"**/.env*": "deny",
2634
}
2735

28-
REQUIRED_APPROVAL_POLICY = "on-request"
36+
REQUIRED_NETWORK_DOMAINS = {
37+
"github.com": "allow",
38+
}
2939

3040
REQUIRED_FORBIDDEN_COMMAND_EXAMPLES = (
3141
"rm -rf",
@@ -76,33 +86,56 @@ def main() -> int:
7686
data = {}
7787
if "prefix_rules" in text:
7888
errors.append(".codex/config.toml: prefix_rules belongs in .codex/rules/*.rules")
79-
if "sandbox_mode" in data and ("default_permissions" in data or "permissions" in data):
80-
errors.append(".codex/config.toml: do not mix sandbox_mode with permission profiles")
8189
if "notify" in data:
8290
errors.append(".codex/config.toml: project config must not set user-level notify")
83-
if data.get("approval_policy") != REQUIRED_APPROVAL_POLICY:
91+
configured_permission_keys = sorted(PROJECT_PERMISSION_KEYS & data.keys())
92+
if configured_permission_keys:
8493
errors.append(
85-
".codex/config.toml: approval_policy must be 'on-request' so the "
86-
"network-restricted handoff workflow can request its authorized push escalation"
94+
".codex/config.toml: the CCGS profile must not override approval policy or use "
95+
"legacy sandbox selection. Remove: "
96+
+ ", ".join(configured_permission_keys)
8797
)
88-
permissions = data.get("permissions")
89-
profile = permissions.get("game_studios") if isinstance(permissions, dict) else None
90-
filesystem = profile.get("filesystem") if isinstance(profile, dict) else None
91-
workspace_rules = filesystem.get(":workspace_roots") if isinstance(filesystem, dict) else None
92-
if not isinstance(workspace_rules, dict):
98+
if data.get("default_permissions") != REQUIRED_DEFAULT_PERMISSIONS:
9399
errors.append(
94-
'.codex/config.toml: missing [permissions.game_studios.filesystem.":workspace_roots"]'
100+
".codex/config.toml: default_permissions must select the complete "
101+
f"{REQUIRED_DEFAULT_PERMISSIONS!r} profile"
95102
)
103+
permissions = data.get("permissions")
104+
profile = permissions.get("game_studios") if isinstance(permissions, dict) else None
105+
if not isinstance(profile, dict):
106+
errors.append(".codex/config.toml: missing [permissions.game_studios] profile")
96107
else:
97-
incorrect_workspace_rules = sorted(
98-
f"{path}={workspace_rules.get(path)!r} (expected {access!r})"
99-
for path, access in REQUIRED_WORKSPACE_RULES.items()
100-
if workspace_rules.get(path) != access
101-
)
102-
if incorrect_workspace_rules:
108+
if profile.get("extends") != ":workspace":
109+
errors.append(
110+
".codex/config.toml: game_studios profile must extend ':workspace'"
111+
)
112+
filesystem = profile.get("filesystem")
113+
workspace_rules = filesystem.get(":workspace_roots") if isinstance(filesystem, dict) else None
114+
if not isinstance(workspace_rules, dict):
115+
errors.append(
116+
'.codex/config.toml: missing [permissions.game_studios.filesystem.":workspace_roots"]'
117+
)
118+
else:
119+
incorrect_workspace_rules = sorted(
120+
f"{path}={workspace_rules.get(path)!r} (expected {access!r})"
121+
for path, access in REQUIRED_WORKSPACE_RULES.items()
122+
if workspace_rules.get(path) != access
123+
)
124+
if incorrect_workspace_rules:
125+
errors.append(
126+
".codex/config.toml: incorrect game_studios workspace rule(s): "
127+
+ ", ".join(incorrect_workspace_rules)
128+
)
129+
network = profile.get("network")
130+
if not isinstance(network, dict) or network.get("enabled") is not True:
131+
errors.append(
132+
".codex/config.toml: game_studios profile must enable its bounded network policy"
133+
)
134+
domains = network.get("domains") if isinstance(network, dict) else None
135+
if domains != REQUIRED_NETWORK_DOMAINS:
103136
errors.append(
104-
".codex/config.toml: incorrect game_studios workspace rule(s): "
105-
+ ", ".join(incorrect_workspace_rules)
137+
".codex/config.toml: game_studios network domains must be exactly "
138+
+ repr(REQUIRED_NETWORK_DOMAINS)
106139
)
107140
tui = data.get("tui")
108141
if isinstance(tui, dict) and "status_line" in tui:

0 commit comments

Comments
 (0)