noah-code 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {noah_code-0.2.0 → noah_code-0.2.1}/.github/workflows/ci.yml +16 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/.github/workflows/release.yml +7 -1
- {noah_code-0.2.0 → noah_code-0.2.1}/PKG-INFO +10 -7
- {noah_code-0.2.0 → noah_code-0.2.1}/README.md +9 -5
- {noah_code-0.2.0 → noah_code-0.2.1}/docs/configuration.md +9 -5
- {noah_code-0.2.0 → noah_code-0.2.1}/docs/extensions.md +19 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/docs/interactive-reference.md +16 -0
- noah_code-0.2.1/docs/releases/v0.2.1.md +36 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/pyproject.toml +1 -2
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/__init__.py +1 -1
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/agent.py +46 -2
- noah_code-0.2.1/src/noah_code/agents.py +112 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/approvals.py +20 -4
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/commands.py +14 -6
- noah_code-0.2.1/src/noah_code/composer.py +127 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/config.py +18 -0
- noah_code-0.2.1/src/noah_code/credentials.py +190 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/host.py +46 -5
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/mcp_setup.py +59 -3
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/permissions.py +124 -41
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/providers.py +9 -3
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/skills_setup.py +5 -2
- noah_code-0.2.1/src/noah_code/tools/__init__.py +17 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/tools/git_tools.py +40 -9
- noah_code-0.2.1/src/noah_code/tools/media_tools.py +31 -0
- noah_code-0.2.1/src/noah_code/tools/question_tools.py +109 -0
- noah_code-0.2.1/src/noah_code/tools/task_tools.py +126 -0
- noah_code-0.2.1/src/noah_code/tools/web_tools.py +176 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/tools/workspace_tools.py +65 -7
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/ui/console.py +7 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/ui/protocol.py +5 -1
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/ui/textual_app.py +114 -5
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_agent_security.py +5 -0
- noah_code-0.2.1/tests/test_agents.py +52 -0
- noah_code-0.2.1/tests/test_approvals.py +29 -0
- noah_code-0.2.1/tests/test_composer.py +75 -0
- noah_code-0.2.1/tests/test_credentials.py +140 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_git_tools.py +51 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_host.py +21 -2
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_mcp_setup.py +41 -2
- noah_code-0.2.1/tests/test_permissions.py +152 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_providers.py +16 -1
- noah_code-0.2.1/tests/test_question_tools.py +54 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_run_exit.py +46 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_skills_setup.py +7 -0
- noah_code-0.2.1/tests/test_task_tools.py +77 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_textual_tui.py +19 -1
- noah_code-0.2.1/tests/test_wave1_e2e.py +197 -0
- noah_code-0.2.1/tests/test_web_tools.py +86 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_workspace_tools.py +37 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/uv.lock +1 -93
- noah_code-0.2.0/src/noah_code/credentials.py +0 -103
- noah_code-0.2.0/src/noah_code/tools/__init__.py +0 -6
- noah_code-0.2.0/tests/test_credentials.py +0 -60
- noah_code-0.2.0/tests/test_permissions.py +0 -70
- {noah_code-0.2.0 → noah_code-0.2.1}/.gitignore +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/docs/development.md +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/docs/releases/v0.1.0.md +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/docs/releases/v0.1.1.md +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/docs/releases/v0.2.0.md +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/docs/security.md +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/install.sh +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/__main__.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/benchmark.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/cli.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/custom_commands.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/event_bridge.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/events.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/llm.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/macos_sandbox.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/sessions.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/snapshots.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/summarization.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/tool_output.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/tools/lsp_tools.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/tools/process_tools.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/ui/__init__.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/ui/textual.css +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/updates.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/usage.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/src/noah_code/workspace.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_cli.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_config.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_custom_commands.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_efficiency.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_event_bridge.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_event_bridge_and_shell.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_installer.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_llm.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_lsp_tools.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_process_tools.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_sessions.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_snapshots.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_summarization.py +0 -0
- {noah_code-0.2.0 → noah_code-0.2.1}/tests/test_updates.py +0 -0
|
@@ -34,6 +34,12 @@ jobs:
|
|
|
34
34
|
- name: Install locked dependencies
|
|
35
35
|
run: uv sync --locked --all-extras --dev
|
|
36
36
|
|
|
37
|
+
- name: Install ripgrep
|
|
38
|
+
run: |
|
|
39
|
+
sudo apt-get update
|
|
40
|
+
sudo apt-get install -y ripgrep
|
|
41
|
+
rg --version
|
|
42
|
+
|
|
37
43
|
- name: Check lock file
|
|
38
44
|
run: uv lock --check
|
|
39
45
|
|
|
@@ -76,6 +82,16 @@ jobs:
|
|
|
76
82
|
- name: Install locked dependencies
|
|
77
83
|
run: uv sync --locked --all-extras --dev
|
|
78
84
|
|
|
85
|
+
- name: Install ripgrep
|
|
86
|
+
run: |
|
|
87
|
+
if [ "${{ runner.os }}" = "Linux" ]; then
|
|
88
|
+
sudo apt-get update
|
|
89
|
+
sudo apt-get install -y ripgrep
|
|
90
|
+
else
|
|
91
|
+
HOMEBREW_NO_AUTO_UPDATE=1 brew install ripgrep
|
|
92
|
+
fi
|
|
93
|
+
rg --version
|
|
94
|
+
|
|
79
95
|
- name: Run tests
|
|
80
96
|
run: uv run pytest tests -W error::pytest.PytestUnraisableExceptionWarning
|
|
81
97
|
|
|
@@ -47,6 +47,12 @@ jobs:
|
|
|
47
47
|
- name: Install locked dependencies
|
|
48
48
|
run: uv sync --locked --all-extras --dev
|
|
49
49
|
|
|
50
|
+
- name: Install ripgrep
|
|
51
|
+
run: |
|
|
52
|
+
sudo apt-get update
|
|
53
|
+
sudo apt-get install -y ripgrep
|
|
54
|
+
rg --version
|
|
55
|
+
|
|
50
56
|
- name: Run release checks
|
|
51
57
|
run: |
|
|
52
58
|
uv run ruff check src tests
|
|
@@ -68,7 +74,7 @@ jobs:
|
|
|
68
74
|
"${UV_TOOL_BIN_DIR}/noah" --version
|
|
69
75
|
|
|
70
76
|
- name: Upload release distributions
|
|
71
|
-
uses: actions/upload-artifact@
|
|
77
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
72
78
|
with:
|
|
73
79
|
name: python-package-distributions
|
|
74
80
|
path: dist/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: noah-code
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Noah Code terminal coding agent, built on NVIDIA OO Agents (NOOA)
|
|
5
5
|
Project-URL: Homepage, https://github.com/skundu42/noah-code
|
|
6
6
|
Project-URL: Documentation, https://github.com/skundu42/noah-code#readme
|
|
@@ -20,7 +20,6 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
20
20
|
Classifier: Topic :: Software Development
|
|
21
21
|
Requires-Python: <3.14,>=3.12
|
|
22
22
|
Requires-Dist: click>=8.1.0
|
|
23
|
-
Requires-Dist: keyring>=25.0.0
|
|
24
23
|
Requires-Dist: litellm<1.92.0,>=1.84.0
|
|
25
24
|
Requires-Dist: nooa-cli==0.0.9
|
|
26
25
|
Requires-Dist: nooa==0.0.9
|
|
@@ -92,6 +91,9 @@ and seccomp support. You also need an LLM provider account such as OpenAI, Anthr
|
|
|
92
91
|
- Inspect token, prompt-cache, model-wait, and tool-output usage with `/tokens`; switch live
|
|
93
92
|
`fast`, `balanced`, and `deep` budgets with `/efficiency`.
|
|
94
93
|
- Switch AI models between turns, with optional cross-repository defaults.
|
|
94
|
+
- Delegate research to nested **explore** and **general** subagents, or markdown agents in
|
|
95
|
+
`.noah-code/agents/`.
|
|
96
|
+
- Fetch pages and search the web, ask structured questions mid-turn, and attach `@files` or images.
|
|
95
97
|
- Extend workflows with slash commands, opt-in skills, MCP servers, model selection, and tracing.
|
|
96
98
|
|
|
97
99
|
## Quick start
|
|
@@ -141,9 +143,10 @@ noah benchmark .
|
|
|
141
143
|
```
|
|
142
144
|
|
|
143
145
|
Bring your own API key from inside the TUI by entering `/model`: choose the provider, paste the
|
|
144
|
-
key into the masked field, choose the model, then select its reasoning effort.
|
|
145
|
-
|
|
146
|
-
API-key values are never written to Noah config,
|
|
146
|
+
key into the masked field, choose the model, then select its reasoning effort. Like OpenCode,
|
|
147
|
+
Noah saves provider credentials in `~/.local/share/noah-code/auth.json`; the directory is
|
|
148
|
+
owner-only and the file uses mode `0600`. API-key values are never written to Noah config,
|
|
149
|
+
repository, or session files. Set `XDG_DATA_HOME` to relocate the data directory.
|
|
147
150
|
|
|
148
151
|
Environment variables and the CLI remain available for scripts and headless use:
|
|
149
152
|
|
|
@@ -167,8 +170,8 @@ Mistral, xAI, DeepSeek, Together AI, and Perplexity are also supported. See the
|
|
|
167
170
|
[provider configuration guide](docs/configuration.md#bring-your-own-api-provider).
|
|
168
171
|
|
|
169
172
|
The package also installs `noah-code` and `nc` as equivalent entry points. Because `nc` commonly
|
|
170
|
-
refers to netcat, `noah` or `noah-code` is recommended. Keep provider API keys in
|
|
171
|
-
|
|
173
|
+
refers to netcat, `noah` or `noah-code` is recommended. Keep provider API keys in Noah's private
|
|
174
|
+
auth file or environment, never in a repository or Noah Code session metadata.
|
|
172
175
|
|
|
173
176
|
Inside a session, bare `/model` opens guided provider, API-key, model, and reasoning setup. `/model MODEL`
|
|
174
177
|
switches the active model immediately and remembers it when that session is resumed. It does not
|
|
@@ -48,6 +48,9 @@ and seccomp support. You also need an LLM provider account such as OpenAI, Anthr
|
|
|
48
48
|
- Inspect token, prompt-cache, model-wait, and tool-output usage with `/tokens`; switch live
|
|
49
49
|
`fast`, `balanced`, and `deep` budgets with `/efficiency`.
|
|
50
50
|
- Switch AI models between turns, with optional cross-repository defaults.
|
|
51
|
+
- Delegate research to nested **explore** and **general** subagents, or markdown agents in
|
|
52
|
+
`.noah-code/agents/`.
|
|
53
|
+
- Fetch pages and search the web, ask structured questions mid-turn, and attach `@files` or images.
|
|
51
54
|
- Extend workflows with slash commands, opt-in skills, MCP servers, model selection, and tracing.
|
|
52
55
|
|
|
53
56
|
## Quick start
|
|
@@ -97,9 +100,10 @@ noah benchmark .
|
|
|
97
100
|
```
|
|
98
101
|
|
|
99
102
|
Bring your own API key from inside the TUI by entering `/model`: choose the provider, paste the
|
|
100
|
-
key into the masked field, choose the model, then select its reasoning effort.
|
|
101
|
-
|
|
102
|
-
API-key values are never written to Noah config,
|
|
103
|
+
key into the masked field, choose the model, then select its reasoning effort. Like OpenCode,
|
|
104
|
+
Noah saves provider credentials in `~/.local/share/noah-code/auth.json`; the directory is
|
|
105
|
+
owner-only and the file uses mode `0600`. API-key values are never written to Noah config,
|
|
106
|
+
repository, or session files. Set `XDG_DATA_HOME` to relocate the data directory.
|
|
103
107
|
|
|
104
108
|
Environment variables and the CLI remain available for scripts and headless use:
|
|
105
109
|
|
|
@@ -123,8 +127,8 @@ Mistral, xAI, DeepSeek, Together AI, and Perplexity are also supported. See the
|
|
|
123
127
|
[provider configuration guide](docs/configuration.md#bring-your-own-api-provider).
|
|
124
128
|
|
|
125
129
|
The package also installs `noah-code` and `nc` as equivalent entry points. Because `nc` commonly
|
|
126
|
-
refers to netcat, `noah` or `noah-code` is recommended. Keep provider API keys in
|
|
127
|
-
|
|
130
|
+
refers to netcat, `noah` or `noah-code` is recommended. Keep provider API keys in Noah's private
|
|
131
|
+
auth file or environment, never in a repository or Noah Code session metadata.
|
|
128
132
|
|
|
129
133
|
Inside a session, bare `/model` opens guided provider, API-key, model, and reasoning setup. `/model MODEL`
|
|
130
134
|
switches the active model immediately and remembers it when that session is resumed. It does not
|
|
@@ -30,9 +30,11 @@ Inside an interactive session, switch only the current session or replace the gl
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Bare `/model` opens a guided TUI flow: search for a provider, enter its API key in a masked
|
|
33
|
-
field, enter the model ID, and select reasoning effort. Noah
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
field, enter the model ID, and select reasoning effort. Noah saves the credential in
|
|
34
|
+
`~/.local/share/noah-code/auth.json`, using the same provider-keyed record shape as OpenCode.
|
|
35
|
+
The containing directory uses mode `0700` and the file uses mode `0600`. If the file cannot be
|
|
36
|
+
written, the key remains active only in the current Noah process and the TUI says so. Keys are
|
|
37
|
+
never written to Noah configuration or session metadata. `XDG_DATA_HOME` relocates the data root.
|
|
36
38
|
|
|
37
39
|
Model switches take effect between turns and are stored in the current session metadata, so a
|
|
38
40
|
resumed session continues with its most recently selected model. A session-only `/model MODEL`
|
|
@@ -237,10 +239,12 @@ Permission rules are evaluated in order, and the last matching rule wins. The de
|
|
|
237
239
|
- Allows ordinary reads.
|
|
238
240
|
- Denies likely secrets, including `.env` variants, private keys, `.git` internals, and session
|
|
239
241
|
databases. `.env.example` remains readable.
|
|
240
|
-
- Asks before workspace edits
|
|
242
|
+
- Asks before workspace edits, shell commands, web fetches, web searches, and subagents.
|
|
243
|
+
- Allows the question tool so the agent can pause for a structured choice.
|
|
241
244
|
- Denies `git push`, `git clean`, and `git reset --hard`.
|
|
242
245
|
- Keeps file tools inside the active workspace and asks before skill or MCP access.
|
|
243
|
-
- Denies plan-mode mutations regardless of broader allow rules.
|
|
246
|
+
- Denies plan-mode mutations regardless of broader allow rules. Plan mode may still run
|
|
247
|
+
read-only subagents.
|
|
244
248
|
|
|
245
249
|
`--auto` changes ask decisions to allow but never overrides an explicit deny. Compound shell
|
|
246
250
|
commands and mutating or unrecognized Git commands cannot be silently auto-approved.
|
|
@@ -22,6 +22,25 @@ test.
|
|
|
22
22
|
Invoke it as `/fix the parser`. Commands support `$ARGUMENTS` and positional placeholders `$1`
|
|
23
23
|
through `$9`. Front matter may also select a mode or model.
|
|
24
24
|
|
|
25
|
+
## Subagents
|
|
26
|
+
|
|
27
|
+
The parent agent invokes nested NOOA agents with `self.task.run("explore", prompt)` or
|
|
28
|
+
`self.task.run("general", prompt)`. Each child gets isolated in-memory session storage and the
|
|
29
|
+
same permission broker. Add project or user markdown agents:
|
|
30
|
+
|
|
31
|
+
- `~/.config/noah-code/agents/*.md`
|
|
32
|
+
- `.noah-code/agents/*.md`
|
|
33
|
+
|
|
34
|
+
```markdown
|
|
35
|
+
---
|
|
36
|
+
description: Review a diff without editing
|
|
37
|
+
readonly: true
|
|
38
|
+
---
|
|
39
|
+
Review the assigned change. Cite files. Do not edit.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`/agents` lists discovered names. Project files override user files with the same stem.
|
|
43
|
+
|
|
25
44
|
## Skills
|
|
26
45
|
|
|
27
46
|
Open the dedicated searchable picker with `/skills` or `Ctrl+K`. Selecting a document skill
|
|
@@ -54,6 +54,8 @@ the configured `max_output_chars` per activity.
|
|
|
54
54
|
| `/tokens` | Show tokens, cache hits, cost, model wait, and tool-output volume |
|
|
55
55
|
| `/efficiency [fast|balanced|deep]` | Show or switch live iteration and output budgets |
|
|
56
56
|
| `/todos` | Show the agent's current task list |
|
|
57
|
+
| `/agents` | List built-in and markdown subagents |
|
|
58
|
+
| `/attach PATH` | Attach a workspace file or image to the next turn |
|
|
57
59
|
| `/status` | Inspect the current session and repository state |
|
|
58
60
|
| `/diff` | Review staged and unstaged files, patches, diagnostics, and changed symbols |
|
|
59
61
|
| `/undo`, `/redo` | Restore or reapply journaled file edits |
|
|
@@ -130,3 +132,17 @@ Long-running commands use `self.processes.start`, `logs`, `status`, `input`, and
|
|
|
130
132
|
owned by the current session, run in separate process groups, have bounded runtime and retained
|
|
131
133
|
output, and are terminated when Noah closes. `logs` accepts a cursor and returns only new output.
|
|
132
134
|
Lifecycle updates appear in the TUI without copying continuous logs into model context.
|
|
135
|
+
|
|
136
|
+
### Subagents, web, questions, and attachments
|
|
137
|
+
|
|
138
|
+
The parent agent can run isolated NOOA subagents with `self.task.run("explore", ...)` or
|
|
139
|
+
`self.task.run("general", ...)`. Explore is read-only. General can edit but does not own todos.
|
|
140
|
+
Custom agents are markdown files in `.noah-code/agents/` or `~/.config/noah-code/agents/`. List
|
|
141
|
+
them with `/agents`. Plan mode can run read-only agents only.
|
|
142
|
+
|
|
143
|
+
`self.web.fetch(url)` and `self.web.search(query)` ask before leaving the machine.
|
|
144
|
+
`self.ask.question(header, prompt, options)` pauses the turn for a structured choice.
|
|
145
|
+
|
|
146
|
+
Type `@path` in the composer to inline a workspace file or attach a PNG/JPEG/WebP/GIF as a NOOA
|
|
147
|
+
`Image` for `show()`. `/attach PATH` does the same from a slash command. Pasting an image path
|
|
148
|
+
into the composer also inserts an `@` mention.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Noah Code v0.2.1
|
|
2
|
+
|
|
3
|
+
This release expands Noah's agentic workflows and hardens the boundaries around credentials,
|
|
4
|
+
generated code, extensions, and repository operations.
|
|
5
|
+
|
|
6
|
+
## Highlights
|
|
7
|
+
|
|
8
|
+
- Added nested `explore` and `general` subagents, project and user agent discovery, and a
|
|
9
|
+
permission-gated task tool for isolated delegated work.
|
|
10
|
+
- Added public web fetch and search tools, structured user questions, and `@file` mentions that
|
|
11
|
+
attach text and images directly to a turn.
|
|
12
|
+
- Added console and Textual support for subagent activity, question prompts, attached media, and
|
|
13
|
+
the new permission categories.
|
|
14
|
+
- Exposed `RespondReason` in the CodeAct execution context so the documented inline completion
|
|
15
|
+
pattern finishes in one model call without a recovery turn.
|
|
16
|
+
|
|
17
|
+
## Security and reliability
|
|
18
|
+
|
|
19
|
+
- Replaced platform-keychain reliance with an owner-only, atomic provider credential store under
|
|
20
|
+
Noah's data directory, while keeping secrets out of configuration and session metadata.
|
|
21
|
+
- Hardened custom-provider credential routing, command parsing, path containment, MCP and skill
|
|
22
|
+
setup, Git operations, generated-code sandboxing, and approval boundaries.
|
|
23
|
+
- Updated CI and release artifact handling, restored explicit ripgrep installation, and expanded
|
|
24
|
+
the hermetic suite to 222 tests.
|
|
25
|
+
|
|
26
|
+
## Upgrade
|
|
27
|
+
|
|
28
|
+
Existing managed installations can run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
noah update
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
New installations can use the one-command installer from the README.
|
|
35
|
+
|
|
36
|
+
**Full changelog:** https://github.com/skundu42/noah-code/compare/v0.2.0...v0.2.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "noah-code"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "Noah Code terminal coding agent, built on NVIDIA OO Agents (NOOA)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12,<3.14"
|
|
@@ -27,7 +27,6 @@ dependencies = [
|
|
|
27
27
|
"click>=8.1.0",
|
|
28
28
|
"pydantic>=2.5.0",
|
|
29
29
|
"PyYAML>=6.0.0",
|
|
30
|
-
"keyring>=25.0.0",
|
|
31
30
|
"packaging>=24.0",
|
|
32
31
|
"tomli>=2.0.0; python_version < '3.11'",
|
|
33
32
|
"rich>=13.0.0",
|
|
@@ -14,7 +14,7 @@ from typing import Annotated, Any, Literal
|
|
|
14
14
|
|
|
15
15
|
from nooa import Context, hidden, strategy
|
|
16
16
|
from nooa.config import CodeActConfig, PredictConfig
|
|
17
|
-
from nooa.interactive import InteractiveAgent, RespondResult
|
|
17
|
+
from nooa.interactive import InteractiveAgent, RespondReason, RespondResult
|
|
18
18
|
from nooa.runtime.restrictions import RESTRICTED_MODULES, RestrictionsConfig
|
|
19
19
|
from nooa.runtime.sandbox.config import FileRule, SandboxConfig, resolve_spec
|
|
20
20
|
from nooa.runtime.sandbox.executor import SandboxedExecutor
|
|
@@ -30,7 +30,11 @@ from noah_code.permissions import PermissionEngine
|
|
|
30
30
|
from noah_code.snapshots import SnapshotJournal
|
|
31
31
|
from noah_code.tools.git_tools import GitTools
|
|
32
32
|
from noah_code.tools.lsp_tools import LSPTools
|
|
33
|
+
from noah_code.tools.media_tools import MediaTools
|
|
33
34
|
from noah_code.tools.process_tools import ProcessTools
|
|
35
|
+
from noah_code.tools.question_tools import QuestionTools
|
|
36
|
+
from noah_code.tools.task_tools import TaskTools
|
|
37
|
+
from noah_code.tools.web_tools import WebTools
|
|
34
38
|
from noah_code.tools.workspace_tools import WorkspaceTools
|
|
35
39
|
from noah_code.workspace import Workspace
|
|
36
40
|
|
|
@@ -115,6 +119,13 @@ class _PermissionSandboxedExecutor(SandboxedExecutor):
|
|
|
115
119
|
("lsp", "rename_preview"),
|
|
116
120
|
("lsp", "repository_map"),
|
|
117
121
|
("lsp", "workspace_symbols"),
|
|
122
|
+
("media", "consume"),
|
|
123
|
+
("media", "pending"),
|
|
124
|
+
("ask", "question"),
|
|
125
|
+
("web", "fetch"),
|
|
126
|
+
("web", "search"),
|
|
127
|
+
("task", "list"),
|
|
128
|
+
("task", "run"),
|
|
118
129
|
("processes", "input"),
|
|
119
130
|
("processes", "logs"),
|
|
120
131
|
("processes", "start"),
|
|
@@ -210,6 +221,13 @@ class _MacOSPermissionSandboxedExecutor(_PermissionSandboxedExecutor):
|
|
|
210
221
|
|
|
211
222
|
|
|
212
223
|
class _PermissionCodeActStrategy(CodeActStrategy):
|
|
224
|
+
def _build_builtins(self, runtime: Any, call: Any) -> dict[str, Any]:
|
|
225
|
+
builtins = super()._build_builtins(runtime, call)
|
|
226
|
+
# InteractiveAgent documents this exact inline return pattern. Keep the
|
|
227
|
+
# enum explicit in case module-context filtering changes upstream.
|
|
228
|
+
builtins["RespondReason"] = RespondReason
|
|
229
|
+
return builtins
|
|
230
|
+
|
|
213
231
|
def _create_sandbox_executor(self, runtime: Any, call: Any, builtins: dict[str, Any]) -> Any:
|
|
214
232
|
framework_builtins = {**builtins, "_call": call}
|
|
215
233
|
executor_type = (
|
|
@@ -287,6 +305,8 @@ class CodingAgent(InteractiveAgent):
|
|
|
287
305
|
engine: PermissionEngine | None = None,
|
|
288
306
|
approvals: ApprovalBroker | None = None,
|
|
289
307
|
journal: SnapshotJournal | None = None,
|
|
308
|
+
nested: bool = False,
|
|
309
|
+
nested_prompt: str | None = None,
|
|
290
310
|
**kwargs: Any,
|
|
291
311
|
) -> None:
|
|
292
312
|
super().__init__(llm=llm, storage=storage, **kwargs)
|
|
@@ -294,6 +314,7 @@ class CodingAgent(InteractiveAgent):
|
|
|
294
314
|
self.workspace_root = str(workspace.root)
|
|
295
315
|
self.mode = config.mode
|
|
296
316
|
self._config = config
|
|
317
|
+
self._nested = nested
|
|
297
318
|
|
|
298
319
|
self._engine = engine or PermissionEngine(
|
|
299
320
|
config.permission_rules,
|
|
@@ -341,11 +362,21 @@ class CodingAgent(InteractiveAgent):
|
|
|
341
362
|
)
|
|
342
363
|
self.todos = TodoManager()
|
|
343
364
|
self.git = GitTools(self.ws)
|
|
365
|
+
self.web = WebTools(self._engine, self._approvals)
|
|
366
|
+
self.ask = QuestionTools(self._engine, self._approvals)
|
|
367
|
+
self.media = MediaTools()
|
|
344
368
|
self._sandbox_approved_roots: set[str] = set()
|
|
369
|
+
if not nested:
|
|
370
|
+
self.task = TaskTools(
|
|
371
|
+
workspace,
|
|
372
|
+
self._engine,
|
|
373
|
+
self._approvals,
|
|
374
|
+
parent=self,
|
|
375
|
+
)
|
|
345
376
|
|
|
346
377
|
from noah_code.skills_setup import install_skills
|
|
347
378
|
|
|
348
|
-
self._skills_status = install_skills(self, workspace.root, config)
|
|
379
|
+
self._skills_status = "" if nested else install_skills(self, workspace.root, config)
|
|
349
380
|
|
|
350
381
|
# Bounded live context - not full trees/diffs.
|
|
351
382
|
self.context["workspace"] = Context(
|
|
@@ -355,6 +386,10 @@ class CodingAgent(InteractiveAgent):
|
|
|
355
386
|
self._git_summary_value = self._git_summary()
|
|
356
387
|
self.context["git"] = Context(expr="self._git_summary_value")
|
|
357
388
|
self.context["background_jobs"] = Context(expr="self.processes.summary()")
|
|
389
|
+
if nested_prompt:
|
|
390
|
+
self.context["subagent"] = Context(nested_prompt, prefix=True)
|
|
391
|
+
if not nested:
|
|
392
|
+
self.context["agents"] = Context(expr="self.task.list()")
|
|
358
393
|
|
|
359
394
|
if config.summarization.policy != "none":
|
|
360
395
|
from nooa.config.summarizer_config import TokenBudgetConfig
|
|
@@ -546,10 +581,19 @@ class CodingAgent(InteractiveAgent):
|
|
|
546
581
|
long-running commands. Consume logs by cursor; do not poll without new work.
|
|
547
582
|
- ``result = await self.ws.run("pytest -q")`` runs validation; inspect
|
|
548
583
|
``result.returncode``, ``result.stdout``, and ``result.stderr``.
|
|
584
|
+
- ``await self.web.fetch(url)`` reads a page; ``await self.web.search(query)``
|
|
585
|
+
searches the public web. Both ask for approval by default.
|
|
586
|
+
- ``await self.ask.question(header, prompt, options)`` pauses for a user choice.
|
|
587
|
+
- ``await self.task.run("explore", "...")`` or ``"general"`` runs a nested
|
|
588
|
+
NOOA subagent with isolated history. ``self.task.list()`` shows markdown agents.
|
|
589
|
+
- If ``self.media.pending()`` is non-empty, ``show()`` each ``self.media.consume()``
|
|
590
|
+
image before reasoning. ``show`` is a CodeAct builtin; do not import ``nooa``.
|
|
549
591
|
|
|
550
592
|
Workflow:
|
|
593
|
+
- If the user attached images, show them first.
|
|
551
594
|
- Inspect relevant repository instructions and nearby code first.
|
|
552
595
|
- Prefer ``self.ws.search`` / focused ``self.ws.read`` over dumping large files.
|
|
596
|
+
- Delegate bounded research or parallel units with ``self.task.run``.
|
|
553
597
|
- Use ``self.todos`` for genuinely multi-step tasks; keep todos current.
|
|
554
598
|
- Make the smallest coherent change, preferring one atomic ``self.ws.apply_patch``.
|
|
555
599
|
- Preserve unrelated user modifications.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Built-in and markdown-defined coding agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from noah_code.custom_commands import _parse_frontmatter
|
|
10
|
+
|
|
11
|
+
AgentKind = Literal["primary", "subagent"]
|
|
12
|
+
AgentMode = Literal["build", "plan"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class AgentSpec:
|
|
17
|
+
"""A specialized agent the parent can invoke with ``self.task.run``."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
description: str
|
|
21
|
+
prompt: str
|
|
22
|
+
kind: AgentKind = "subagent"
|
|
23
|
+
mode: AgentMode = "build"
|
|
24
|
+
readonly: bool = False
|
|
25
|
+
todos: bool = True
|
|
26
|
+
model: str | None = None
|
|
27
|
+
source: str = "builtin"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def builtin_agents() -> list[AgentSpec]:
|
|
31
|
+
"""OpenCode-style Explore and General subagents."""
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
AgentSpec(
|
|
35
|
+
name="explore",
|
|
36
|
+
description="Fast read-only agent for finding files, searching code, and answering codebase questions.",
|
|
37
|
+
prompt=(
|
|
38
|
+
"You are a fast, read-only explore agent. Do not modify files or run "
|
|
39
|
+
"mutating commands. Search and read until you can answer with file paths "
|
|
40
|
+
"and short evidence. Prefer self.ws.search, self.ws.read, and self.lsp."
|
|
41
|
+
),
|
|
42
|
+
kind="subagent",
|
|
43
|
+
mode="plan",
|
|
44
|
+
readonly=True,
|
|
45
|
+
todos=False,
|
|
46
|
+
source="builtin",
|
|
47
|
+
),
|
|
48
|
+
AgentSpec(
|
|
49
|
+
name="general",
|
|
50
|
+
description="General-purpose subagent for researching complex questions and executing multi-step work in parallel.",
|
|
51
|
+
prompt=(
|
|
52
|
+
"You are a focused general-purpose subagent. Complete the assigned unit of "
|
|
53
|
+
"work and return a concise result to the parent. Do not manage todos. "
|
|
54
|
+
"Make the smallest coherent change and report what you did."
|
|
55
|
+
),
|
|
56
|
+
kind="subagent",
|
|
57
|
+
mode="build",
|
|
58
|
+
readonly=False,
|
|
59
|
+
todos=False,
|
|
60
|
+
source="builtin",
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def discover_agents(workspace: Path, *, home: Path | None = None) -> list[AgentSpec]:
|
|
66
|
+
"""Built-ins plus user and project markdown agents. Project names win."""
|
|
67
|
+
|
|
68
|
+
found = {spec.name: spec for spec in builtin_agents()}
|
|
69
|
+
user_home = (home or Path.home()).expanduser()
|
|
70
|
+
user_dir = user_home / ".config" / "noah-code" / "agents"
|
|
71
|
+
project_dir = workspace / ".noah-code" / "agents"
|
|
72
|
+
for directory, source in ((user_dir, "user"), (project_dir, "project")):
|
|
73
|
+
found.update(_load_markdown_agents(directory, source=source))
|
|
74
|
+
return list(found.values())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _load_markdown_agents(directory: Path, *, source: str) -> dict[str, AgentSpec]:
|
|
78
|
+
out: dict[str, AgentSpec] = {}
|
|
79
|
+
if not directory.is_dir():
|
|
80
|
+
return out
|
|
81
|
+
for path in sorted(directory.glob("*.md")):
|
|
82
|
+
name = path.stem.strip().lower().lstrip("/")
|
|
83
|
+
if not name or name.startswith("."):
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
raw = path.read_text(encoding="utf-8")
|
|
87
|
+
except OSError:
|
|
88
|
+
continue
|
|
89
|
+
meta, body = _parse_frontmatter(raw)
|
|
90
|
+
mode_raw = str(meta.get("mode") or "build").strip().lower()
|
|
91
|
+
mode: AgentMode = "plan" if mode_raw in {"plan", "readonly", "read-only"} else "build"
|
|
92
|
+
readonly = _truthy(meta.get("readonly")) or mode == "plan"
|
|
93
|
+
out[name] = AgentSpec(
|
|
94
|
+
name=name,
|
|
95
|
+
description=str(meta.get("description") or name),
|
|
96
|
+
prompt=body.strip(),
|
|
97
|
+
kind="subagent",
|
|
98
|
+
mode="plan" if readonly else mode,
|
|
99
|
+
readonly=readonly,
|
|
100
|
+
todos=_truthy(meta.get("todos"), default=False),
|
|
101
|
+
model=str(meta["model"]) if meta.get("model") else None,
|
|
102
|
+
source=f"{source}:{path.name}",
|
|
103
|
+
)
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _truthy(value: object, *, default: bool = False) -> bool:
|
|
108
|
+
if value is None or value == "":
|
|
109
|
+
return default
|
|
110
|
+
if isinstance(value, bool):
|
|
111
|
+
return value
|
|
112
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import contextlib
|
|
6
7
|
import uuid
|
|
7
8
|
from collections.abc import Awaitable, Callable
|
|
8
9
|
from dataclasses import dataclass, field
|
|
@@ -90,10 +91,25 @@ class ApprovalBroker:
|
|
|
90
91
|
if self._handler is None:
|
|
91
92
|
# Non-interactive without --auto: treat ask as deny.
|
|
92
93
|
return ApprovalChoice.REJECT
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
|
|
95
|
+
async def _resolve() -> None:
|
|
96
|
+
try:
|
|
97
|
+
choice = await self._handler(request)
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
if not fut.done():
|
|
100
|
+
fut.set_exception(exc)
|
|
101
|
+
return
|
|
102
|
+
if not fut.done():
|
|
103
|
+
fut.set_result(choice)
|
|
104
|
+
|
|
105
|
+
task = asyncio.create_task(_resolve())
|
|
106
|
+
try:
|
|
107
|
+
return await fut
|
|
108
|
+
finally:
|
|
109
|
+
if not task.done():
|
|
110
|
+
task.cancel()
|
|
111
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
112
|
+
await task
|
|
97
113
|
finally:
|
|
98
114
|
async with self._lock:
|
|
99
115
|
self._pending.pop(req_id, None)
|
|
@@ -31,7 +31,9 @@ BUILTIN_COMMANDS: list[CommandSpec] = [
|
|
|
31
31
|
CommandSpec("help", "Show available commands", host_only=True),
|
|
32
32
|
CommandSpec("config", "Show every resolved setting or one path", "config [PATH]", True),
|
|
33
33
|
CommandSpec("mode", "Show or switch the active mode", "mode [build|plan]", True),
|
|
34
|
-
CommandSpec(
|
|
34
|
+
CommandSpec(
|
|
35
|
+
"model", "Configure a provider or switch this session's model", "model [MODEL]", True
|
|
36
|
+
),
|
|
35
37
|
CommandSpec(
|
|
36
38
|
"reasoning",
|
|
37
39
|
"Show or set reasoning effort for compatible models",
|
|
@@ -61,7 +63,13 @@ BUILTIN_COMMANDS: list[CommandSpec] = [
|
|
|
61
63
|
CommandSpec("diff", "Review staged and unstaged changes", host_only=True),
|
|
62
64
|
CommandSpec("undo", "Undo last WorkspaceTools turn", host_only=True),
|
|
63
65
|
CommandSpec("redo", "Redo last undone turn", host_only=True),
|
|
64
|
-
CommandSpec("
|
|
66
|
+
CommandSpec("agents", "List built-in and markdown subagents", host_only=True),
|
|
67
|
+
CommandSpec(
|
|
68
|
+
"attach", "Attach a workspace file or image to the next turn", "attach [PATH]", True
|
|
69
|
+
),
|
|
70
|
+
CommandSpec(
|
|
71
|
+
"skills", "Search skills or add a compatible skill folder", "skills [add PATH]", True
|
|
72
|
+
),
|
|
65
73
|
CommandSpec("mcp", "Search, connect, or add MCP servers", "mcp [connect|add]", True),
|
|
66
74
|
CommandSpec("trace", "Show tracing destination", host_only=True),
|
|
67
75
|
CommandSpec("exit", "Exit Noah Code", host_only=True),
|
|
@@ -127,7 +135,9 @@ def config_text(config: Any, path: str = "") -> str:
|
|
|
127
135
|
if not rows:
|
|
128
136
|
raise KeyError(path)
|
|
129
137
|
width = max(len(key) for key, _value in rows)
|
|
130
|
-
title =
|
|
138
|
+
title = (
|
|
139
|
+
f"Resolved configuration ({path.strip()}):" if path.strip() else "Resolved configuration:"
|
|
140
|
+
)
|
|
131
141
|
return "\n".join([title, "", *(f" {key:<{width}} {value}" for key, value in rows)])
|
|
132
142
|
|
|
133
143
|
|
|
@@ -167,9 +177,7 @@ def help_text(custom: dict[str, CustomCommand] | None = None) -> str:
|
|
|
167
177
|
for cmd in BUILTIN_COMMANDS:
|
|
168
178
|
lines.append(f" {cmd.invocation:<28} {cmd.description}")
|
|
169
179
|
lines.append(f" {'/model --global MODEL':<28} Set the default model for every repository")
|
|
170
|
-
lines.append(
|
|
171
|
-
f" {'/reasoning --global EFFORT':<28} Set the global reasoning effort default"
|
|
172
|
-
)
|
|
180
|
+
lines.append(f" {'/reasoning --global EFFORT':<28} Set the global reasoning effort default")
|
|
173
181
|
if custom:
|
|
174
182
|
lines.append("")
|
|
175
183
|
lines.append("Custom commands:")
|