Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 6 additions & 2 deletions console/web/src/lib/backend/harness-send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ export type HarnessOutputContract =
| { type: 'text' }
| { type: 'json'; schema?: unknown }

/** Operating mode — the harness prepends a short paragraph before the identity prompt. */
export type HarnessSendMode = 'plan' | 'ask' | 'agent'
/**
* Operating mode — the harness prepends a short paragraph before the identity
* prompt. `code` instead swaps in the coding identity and, when no explicit
* `functions` policy is sent, defaults to the native coding toolset.
*/
export type HarnessSendMode = 'plan' | 'ask' | 'agent' | 'code'

/** Per-send options frozen onto the turn record. */
export interface HarnessSendOptions {
Expand Down
4 changes: 3 additions & 1 deletion console/web/src/lib/backend/real.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ async function* realStream(
session: { metadata: { surface: 'console' } },
options: {
mode,
functions: functionPolicy,
// Code mode omits `functions` so the harness applies its curated
// native coding policy; other modes keep the gate-configured surface.
...(mode === 'code' ? {} : { functions: functionPolicy }),
...(thinkingLevel ? { thinking_level: thinkingLevel } : {}),
metadata: buildTurnMetadata(sessionId, messageId, opts?.workingDir),
},
Expand Down
3 changes: 2 additions & 1 deletion console/web/src/types/chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Mode = 'plan' | 'ask' | 'agent'
export type Mode = 'plan' | 'ask' | 'agent' | 'code'

/** Composite `provider::<catalog_model_id>` (matches harness models-catalog). */
export const CATALOG_MODEL_KEY_SEP = '::' as const
Expand All @@ -15,6 +15,7 @@ export const MODES: { id: Mode; label: string }[] = [
{ id: 'plan', label: 'plan' },
{ id: 'ask', label: 'ask' },
{ id: 'agent', label: 'agent' },
{ id: 'code', label: 'code' },
]

export const DEFAULT_MODE: Mode = 'agent'
Expand Down
2 changes: 1 addition & 1 deletion harness/prompts/code.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Your tools are attached natively to this conversation: file operations (the code

# Workspace

An <environment> block in this prompt describes your workspace: the working directory (all relative paths resolve there), the platform, the git state, and any repo instruction files (AGENTS.md / CLAUDE.md). Treat repo instruction files as authoritative conventions for this codebase — follow them even when they conflict with your defaults. Paths outside the workspace jail are rejected; stay inside it.
An <environment> block in this prompt describes your workspace: the working directory (all relative paths resolve there), the platform, the git state, and any repo instruction files (AGENTS.md / CLAUDE.md). It is refreshed at the start of every turn — do not call coder::context to re-fetch what it already tells you; call it only when you have changed the git state mid-turn and need a fresh snapshot. Treat repo instruction files as authoritative conventions for this codebase — follow them even when they conflict with your defaults. Paths outside the workspace jail are rejected; stay inside it.

# Working on code

Expand Down
38 changes: 27 additions & 11 deletions shell/src/code/functions/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ fn example_context_input() -> serde_json::Value {

#[derive(Debug, Serialize, JsonSchema)]
pub struct ContextOutput {
/// The primary allowed root — relative paths in every `coder::*` call
/// resolve against this directory.
/// The effective root for this call — the session's working directory
/// when the call carries a filesystem scope, else the primary allowed
/// root. Relative paths in `coder::*` calls resolve against it, and the
/// git state and instruction files below describe it.
pub primary_root: String,
/// Canonical absolute paths of all allowed roots, primary first.
pub base_paths: Vec<String>,
Expand Down Expand Up @@ -93,15 +95,25 @@ pub struct InstructionFile {
pub async fn handle(
resolver: Arc<PathResolver>,
_cfg: Arc<CoderConfig>,
_req: ContextInput,
req: ContextInput,
) -> Result<ContextOutput, String> {
let primary = resolver.base_root().to_path_buf();
// The effective root is the call's fs_scope root when the harness
// stamped one (the turn's working directory) — the configured primary
// only when the call is unscoped. session_root canonicalizes and
// confines it to the allowed roots, exactly like resolve_in.
let scope_root = crate::fs::scope_root(req.fs_scope.as_ref()).map(str::to_string);
let primary = scope_root
.as_deref()
.and_then(|r| resolver.session_root(r))
.unwrap_or_else(|| resolver.base_root().to_path_buf());
let git = git_context(&primary).await;
let resolver_for_files = resolver.clone();
let instruction_files =
tokio::task::spawn_blocking(move || read_instruction_files(&resolver_for_files))
.await
.map_err(|e| format!("context task join failed: {e}"))?;
let files_scope = scope_root.clone();
let instruction_files = tokio::task::spawn_blocking(move || {
read_instruction_files(&resolver_for_files, files_scope.as_deref())
})
.await
.map_err(|e| format!("context task join failed: {e}"))?;

Ok(ContextOutput {
primary_root: primary.display().to_string(),
Expand Down Expand Up @@ -164,11 +176,15 @@ async fn git_context(root: &Path) -> Option<GitContext> {
})
}

fn read_instruction_files(resolver: &PathResolver) -> Vec<InstructionFile> {
fn read_instruction_files(
resolver: &PathResolver,
scope_root: Option<&str>,
) -> Vec<InstructionFile> {
let mut out = Vec::new();
for name in INSTRUCTION_FILE_NAMES {
// Resolve through the jail so non-accessible globs keep applying.
let Ok(abs) = resolver.resolve(name) else {
// Resolve through the jail (relative to the effective root) so
// non-accessible globs keep applying.
let Ok(abs) = resolver.resolve_opt(scope_root, name) else {
continue;
};
if resolver.is_non_accessible(&abs) {
Expand Down
42 changes: 42 additions & 0 deletions shell/tests/code_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,45 @@ async fn context_caps_oversized_instruction_file() {
assert!(out.instruction_files[0].truncated);
assert_eq!(out.instruction_files[0].content.len(), 16 * 1024);
}

#[tokio::test]
async fn context_honors_fs_scope_root() {
let tmp = tempdir().unwrap();
let jail = tmp.path();
let proj = jail.join("proj");
std::fs::create_dir(&proj).unwrap();
git(&proj, &["init", "-b", "work"]);
std::fs::write(proj.join("AGENTS.md"), "scoped conventions\n").unwrap();
git(&proj, &["add", "."]);
git(&proj, &["commit", "-m", "scoped seed"]);

let (r, c) = make(jail.to_path_buf());
// Mirror the registration path: the harness stamps fs_scope and the
// wrapper widens the resolver with session_scoped before handling.
let scope = proj.to_string_lossy().to_string();
let scoped = r.session_scoped(Some(&scope), None);
let out = context_handle(
scoped,
c,
ContextInput {
fs_scope: Some(shell::fs::FsScope {
root: scope,
grants: vec![],
}),
},
)
.await
.unwrap();

assert!(
out.primary_root.ends_with("/proj"),
"effective root is the fs_scope root, got {}",
out.primary_root
);
let git_ctx = out.git.expect("scoped root is a git repo");
assert_eq!(git_ctx.branch, "work");
assert_eq!(out.instruction_files.len(), 1);
assert!(out.instruction_files[0]
.content
.contains("scoped conventions"));
}
2 changes: 1 addition & 1 deletion shell/tests/golden/schemas/coder.context.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"$ref": "#/definitions/Platform"
},
"primary_root": {
"description": "The primary allowed root — relative paths in every `coder::*` call resolve against this directory.",
"description": "The effective root for this call — the session's working directory when the call carries a filesystem scope, else the primary allowed root. Relative paths in `coder::*` calls resolve against it, and the git state and instruction files below describe it.",
"type": "string"
}
},
Expand Down
Loading