Skip to content

Commit 5fd3e6d

Browse files
author
SqlRush
committed
feat(core): add permissions-instructions renderer (input env-context building block)
First piece of the `input` context-fragment system (the largest remaining request-body gap). RenderPermissionsInstructions ports codex's PermissionsInstructions fragment (permissions_instructions.rs + fragment.rs): the developer-role `<permissions instructions>` block built from the sandbox mode, network access, and approval policy, with the prompt texts embedded byte-for-byte from codex's prompts/permissions/*.md and codex's append_section / marker-wrapping semantics replicated exactly. Unit-tested against the real codex 0.136.0 captured output: the default `codex exec` config (read-only + network restricted + approval never) renders byte-identically. Not yet wired into the turn input (next: environment_context renderer + injection into the session initial history + structural differential); tracked in docs/PARITY.md.
1 parent 200c0e6 commit 5fd3e6d

9 files changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package core
2+
3+
import (
4+
_ "embed"
5+
"strings"
6+
7+
"github.com/sqlrush/codexgo/internal/protocol"
8+
)
9+
10+
// This file ports codex's permissions-instructions fragment
11+
// (core/src/context/permissions_instructions.rs): the developer-role
12+
// `<permissions instructions>` message that tells the model the active sandbox
13+
// mode, network access, and approval policy. The prompt texts are embedded
14+
// byte-for-byte from codex's prompts/permissions/*.md.
15+
//
16+
// Scope: the base sandbox-mode × approval-policy matrix used by `codex exec`
17+
// (the request_permissions_tool wrapper, auto_review suffix, writable-roots and
18+
// denied-reads sections are conditional refinements tracked in docs/PARITY.md and
19+
// added with the full permission-profile wiring).
20+
21+
//go:embed prompts/permissions/sandbox_mode/read_only.md
22+
var sandboxReadOnlyTemplate string
23+
24+
//go:embed prompts/permissions/sandbox_mode/workspace_write.md
25+
var sandboxWorkspaceWriteTemplate string
26+
27+
//go:embed prompts/permissions/sandbox_mode/danger_full_access.md
28+
var sandboxDangerFullAccessTemplate string
29+
30+
//go:embed prompts/permissions/approval_policy/never.md
31+
var approvalNeverText string
32+
33+
//go:embed prompts/permissions/approval_policy/unless_trusted.md
34+
var approvalUnlessTrustedText string
35+
36+
//go:embed prompts/permissions/approval_policy/on_failure.md
37+
var approvalOnFailureText string
38+
39+
//go:embed prompts/permissions/approval_policy/on_request.md
40+
var approvalOnRequestText string
41+
42+
const (
43+
permissionsStartMarker = "<permissions instructions>"
44+
permissionsEndMarker = "</permissions instructions>"
45+
networkAccessKey = "{{network_access}}"
46+
)
47+
48+
// PermissionsSandboxMode is the effective filesystem sandbox mode for the prompt,
49+
// mirroring the Rust SandboxMode the prompt selects on (read-only /
50+
// workspace-write / danger-full-access).
51+
type PermissionsSandboxMode int
52+
53+
const (
54+
// SandboxModeReadOnly permits only reads.
55+
SandboxModeReadOnly PermissionsSandboxMode = iota
56+
// SandboxModeWorkspaceWrite permits writes to cwd/writable roots.
57+
SandboxModeWorkspaceWrite
58+
// SandboxModeDangerFullAccess disables filesystem sandboxing.
59+
SandboxModeDangerFullAccess
60+
)
61+
62+
// networkAccessWord renders the {{network_access}} value, matching codex's
63+
// kebab-case NetworkAccess Display ("restricted" / "enabled").
64+
func networkAccessWord(enabled bool) string {
65+
if enabled {
66+
return "enabled"
67+
}
68+
return "restricted"
69+
}
70+
71+
// sandboxPromptText renders the sandbox-mode section, replacing {{network_access}}
72+
// in the embedded template. Mirrors Rust `sandbox_text`.
73+
func sandboxPromptText(mode PermissionsSandboxMode, networkEnabled bool) string {
74+
var tmpl string
75+
switch mode {
76+
case SandboxModeDangerFullAccess:
77+
tmpl = sandboxDangerFullAccessTemplate
78+
case SandboxModeWorkspaceWrite:
79+
tmpl = sandboxWorkspaceWriteTemplate
80+
default:
81+
tmpl = sandboxReadOnlyTemplate
82+
}
83+
return strings.ReplaceAll(tmpl, networkAccessKey, networkAccessWord(networkEnabled))
84+
}
85+
86+
// approvalPromptText returns the approval-policy section for the given policy
87+
// kind, mirroring the base branches of Rust `approval_text` (without the
88+
// conditional request_permissions_tool wrapper / auto_review suffix).
89+
func approvalPromptText(policy protocol.AskForApprovalKind) string {
90+
switch policy {
91+
case protocol.AskForApprovalUnlessTrusted:
92+
return approvalUnlessTrustedText
93+
case protocol.AskForApprovalOnFailure:
94+
return approvalOnFailureText
95+
case protocol.AskForApprovalOnRequest:
96+
return approvalOnRequestText
97+
default: // AskForApprovalNever
98+
return approvalNeverText
99+
}
100+
}
101+
102+
// appendPermissionsSection mirrors Rust `append_section` exactly: if the
103+
// accumulator does not end with a newline (an empty string included), push one,
104+
// then append the section. So the first section is prefixed with '\n' and
105+
// subsequent sections are single-newline separated.
106+
func appendPermissionsSection(text, section string) string {
107+
if !strings.HasSuffix(text, "\n") {
108+
text += "\n"
109+
}
110+
return text + section
111+
}
112+
113+
// RenderPermissionsInstructions builds the full `<permissions instructions>`
114+
// developer message body for the active sandbox/network/approval settings,
115+
// mirroring codex's PermissionsInstructions::from_permissions + fragment render
116+
// (start_marker + body + end_marker, newlines carried in the body).
117+
func RenderPermissionsInstructions(mode PermissionsSandboxMode, networkEnabled bool, policy protocol.AskForApprovalKind) string {
118+
text := appendPermissionsSection("", sandboxPromptText(mode, networkEnabled))
119+
text = appendPermissionsSection(text, approvalPromptText(policy))
120+
if !strings.HasSuffix(text, "\n") {
121+
text += "\n"
122+
}
123+
return permissionsStartMarker + text + permissionsEndMarker
124+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package core
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/sqlrush/codexgo/internal/protocol"
8+
)
9+
10+
// TestRenderPermissionsInstructionsReadOnlyNever pins the exact `<permissions
11+
// instructions>` block codex emits for the default `codex exec` config
12+
// (read-only sandbox, network restricted, approval never) — captured byte-for-byte
13+
// from the real codex 0.136.0 binary's /responses request.
14+
func TestRenderPermissionsInstructionsReadOnlyNever(t *testing.T) {
15+
const want = "<permissions instructions>\n" +
16+
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The sandbox only permits reading files. Network access is restricted.\n" +
17+
"Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.\n" +
18+
"</permissions instructions>"
19+
20+
got := RenderPermissionsInstructions(SandboxModeReadOnly, false, protocol.AskForApprovalNever)
21+
if got != want {
22+
t.Errorf("permissions instructions mismatch\n want: %q\n got: %q", want, got)
23+
}
24+
}
25+
26+
// TestRenderPermissionsInstructionsStructure checks the marker wrapping and
27+
// section ordering invariants across the sandbox modes: the body opens after the
28+
// start marker on its own line, the sandbox line precedes the approval line, and
29+
// the network word reflects the flag.
30+
func TestRenderPermissionsInstructionsStructure(t *testing.T) {
31+
cases := []struct {
32+
name string
33+
mode PermissionsSandboxMode
34+
network bool
35+
wantSandbox string
36+
wantNet string
37+
}{
38+
{"read-only restricted", SandboxModeReadOnly, false, "`read-only`", "restricted"},
39+
{"workspace-write enabled", SandboxModeWorkspaceWrite, true, "`workspace-write`", "enabled"},
40+
{"danger enabled", SandboxModeDangerFullAccess, true, "`danger-full-access`", "enabled"},
41+
}
42+
for _, tc := range cases {
43+
t.Run(tc.name, func(t *testing.T) {
44+
got := RenderPermissionsInstructions(tc.mode, tc.network, protocol.AskForApprovalNever)
45+
if !strings.HasPrefix(got, permissionsStartMarker+"\n") {
46+
t.Errorf("missing start marker + newline; got prefix %q", got[:min(60, len(got))])
47+
}
48+
if !strings.HasSuffix(got, "\n"+permissionsEndMarker) {
49+
t.Errorf("missing newline + end marker; got suffix %q", got[max(0, len(got)-60):])
50+
}
51+
if !strings.Contains(got, tc.wantSandbox) {
52+
t.Errorf("expected sandbox marker %q in %q", tc.wantSandbox, got)
53+
}
54+
if !strings.Contains(got, "Network access is "+tc.wantNet+".") {
55+
t.Errorf("expected network %q in %q", tc.wantNet, got)
56+
}
57+
sandboxIdx := strings.Index(got, "Filesystem sandboxing")
58+
approvalIdx := strings.Index(got, "Approval policy is currently never")
59+
if sandboxIdx < 0 || approvalIdx < 0 || sandboxIdx > approvalIdx {
60+
t.Errorf("sandbox section must precede approval section; got %q", got)
61+
}
62+
})
63+
}
64+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Approvals are your mechanism to get user consent to run shell commands without the sandbox. `approval_policy` is `on-failure`: The harness will allow all commands to run in the sandbox (if enabled), and failures will be escalated to the user for approval to run again without the sandbox.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Escalation Requests
2+
3+
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that allows it to run unrestricted. The command string is split into independent command segments at shell control operators, including but not limited to:
4+
5+
- Pipes: |
6+
- Logical operators: &&, ||
7+
- Command separators: ;
8+
- Subshell boundaries: (...), $(...)
9+
10+
Each resulting segment is evaluated independently for sandbox restrictions and approval requirements.
11+
12+
Example:
13+
14+
git pull | tee output.txt
15+
16+
This is treated as two command segments:
17+
18+
["git", "pull"]
19+
20+
["tee", "output.txt"]
21+
22+
Commands that use more advanced shell features like redirection (>, >>, <), substitutions ($(...), ...), environment variables (FOO=bar), or wildcard patterns (*, ?) will not be evaluated against rules, to limit the scope of what an approved rule allows.
23+
24+
## How to request escalation
25+
26+
IMPORTANT: To request approval to execute a command that will require escalated privileges:
27+
28+
- Provide the `sandbox_permissions` parameter with the value `"require_escalated"`
29+
- Include a short question asking the user if they want to allow the action in `justification` parameter. e.g. "Do you want to download and install dependencies for this project?"
30+
- Optionally suggest a `prefix_rule` - this will be shown to the user with an option to persist the rule approval for future sessions.
31+
32+
If you run a command that is important to solving the user's query, but it fails because of sandboxing or with a likely sandbox-related network error (for example DNS/host resolution, registry/index access, or dependency download failure), rerun the command with "require_escalated". ALWAYS proceed to use the `justification` parameter - do not message the user before requesting approval for the command.
33+
34+
## When to request escalation
35+
36+
While commands are running inside the sandbox, here are some scenarios that will require escalation outside the sandbox:
37+
38+
- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)
39+
- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.
40+
- If you run a command that is important to solving the user's query, but it fails because of sandboxing or with a likely sandbox-related network error (for example DNS/host resolution, registry/index access, or dependency download failure), rerun the command with `require_escalated`. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters. do not message the user before requesting approval for the command.
41+
- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for.
42+
- Be judicious with escalating, but if completing the user's request requires it, you should do so - don't try and circumvent approvals by using other tools.
43+
44+
## prefix_rule guidance
45+
46+
When choosing a `prefix_rule`, request one that will allow you to fulfill similar requests from the user in the future without re-requesting escalation. It should be categorical and reasonably scoped to similar capabilities. You should rarely pass the entire command into `prefix_rule`.
47+
48+
### Banned prefix_rules
49+
Avoid requesting overly broad prefixes that the user would be ill-advised to approve. For example, do not request ["python3"], ["python", "-"], or other similar prefixes that would allow arbitrary scripting.
50+
NEVER provide a prefix_rule argument for destructive commands like rm.
51+
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
52+
53+
### Examples
54+
Good examples of prefixes:
55+
- ["npm", "run", "dev"]
56+
- ["gh", "pr", "check"]
57+
- ["cargo", "test"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Approvals are your mechanism to get user consent to run shell commands without the sandbox. `approval_policy` is `unless-trusted`: The harness will escalate most commands for user approval, apart from a limited allowlist of safe "read" commands.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`: No filesystem sandboxing - all commands are permitted. Network access is {{network_access}}.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The sandbox only permits reading files. Network access is {{network_access}}.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is {{network_access}}.

0 commit comments

Comments
 (0)