nebula-ai-core 0.1.0
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.
- package/README.md +24 -0
- package/package.json +69 -0
- package/src/brain/compaction.ts +131 -0
- package/src/brain/frozen-prefix.ts +320 -0
- package/src/brain/history-persist.ts +154 -0
- package/src/brain/index.ts +43 -0
- package/src/brain/openai-brain.ts +533 -0
- package/src/brain/sanitize.ts +23 -0
- package/src/brain/stub.ts +20 -0
- package/src/brain/types.ts +129 -0
- package/src/chain.ts +75 -0
- package/src/claude-plugins/discovery.ts +152 -0
- package/src/claude-plugins/index.ts +6 -0
- package/src/claude-plugins/types.ts +38 -0
- package/src/commands/index.ts +16 -0
- package/src/commands/registry.ts +255 -0
- package/src/config.ts +213 -0
- package/src/economy/index.ts +6 -0
- package/src/events/index.ts +4 -0
- package/src/events/listeners.ts +37 -0
- package/src/events/queue.ts +63 -0
- package/src/events/router.ts +42 -0
- package/src/events/types.ts +28 -0
- package/src/format.ts +12 -0
- package/src/identity/agent-card.ts +110 -0
- package/src/identity/deployments.ts +20 -0
- package/src/identity/erc8004.ts +161 -0
- package/src/identity/index.ts +29 -0
- package/src/identity/keystore-blob.ts +60 -0
- package/src/identity/receipt.ts +27 -0
- package/src/identity/stub.ts +29 -0
- package/src/identity/types.ts +20 -0
- package/src/index.ts +372 -0
- package/src/locks.ts +233 -0
- package/src/mcp/discovery.ts +150 -0
- package/src/mcp/index.ts +10 -0
- package/src/mcp/manager.ts +110 -0
- package/src/mcp/stdio-client.ts +154 -0
- package/src/mcp/types.ts +44 -0
- package/src/memory/edit.ts +53 -0
- package/src/memory/encryption.ts +88 -0
- package/src/memory/fs-util.ts +15 -0
- package/src/memory/index-file.ts +74 -0
- package/src/memory/index-sync.ts +99 -0
- package/src/memory/index.ts +58 -0
- package/src/memory/list-tool.ts +105 -0
- package/src/memory/pack-blob.ts +120 -0
- package/src/memory/pack-gather.ts +112 -0
- package/src/memory/parser.ts +20 -0
- package/src/memory/read-tool.ts +198 -0
- package/src/memory/save-tool.ts +189 -0
- package/src/memory/scan.ts +63 -0
- package/src/memory/topic.ts +32 -0
- package/src/memory/types.ts +49 -0
- package/src/migration/index.ts +6 -0
- package/src/migration/option3-crypto.ts +127 -0
- package/src/operator/index.ts +9 -0
- package/src/operator/keychain.ts +53 -0
- package/src/operator/keystore-file.ts +33 -0
- package/src/operator/privkey-base.ts +60 -0
- package/src/operator/raw-privkey.ts +39 -0
- package/src/operator/signer.ts +46 -0
- package/src/operator/walletconnect.ts +454 -0
- package/src/pairing.ts +285 -0
- package/src/paths.ts +70 -0
- package/src/permission/dangerous.ts +108 -0
- package/src/permission/env-redact.ts +54 -0
- package/src/permission/index.ts +16 -0
- package/src/permission/path-guard.ts +114 -0
- package/src/permission/service.ts +191 -0
- package/src/plugins/context.ts +225 -0
- package/src/plugins/hooks.ts +81 -0
- package/src/plugins/index.ts +24 -0
- package/src/plugins/tool-search.ts +49 -0
- package/src/public/card.ts +67 -0
- package/src/runtime/activity.ts +29 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/runtime.ts +113 -0
- package/src/sandbox/credentials.ts +25 -0
- package/src/sandbox/docker.ts +396 -0
- package/src/sandbox/factory.ts +99 -0
- package/src/sandbox/index.ts +15 -0
- package/src/sandbox/linux.ts +141 -0
- package/src/sandbox/local.ts +19 -0
- package/src/sandbox/macos.ts +71 -0
- package/src/sandbox/seatbelt-profile.ts +139 -0
- package/src/sandbox/types.ts +129 -0
- package/src/skills/index.ts +8 -0
- package/src/skills/scanner.ts +257 -0
- package/src/skills/triggers.ts +78 -0
- package/src/skills/types.ts +37 -0
- package/src/storage/encryption.ts +87 -0
- package/src/storage/factory.ts +31 -0
- package/src/storage/index.ts +11 -0
- package/src/storage/local-stub.ts +70 -0
- package/src/storage/sqlite.ts +95 -0
- package/src/storage/types.ts +21 -0
- package/src/tools/escalation.ts +200 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/registry.ts +152 -0
- package/src/tools/types.ts +65 -0
- package/src/tools/zod-helpers.ts +36 -0
- package/src/tools/zod-schema.ts +99 -0
- package/src/wallet/drain.ts +79 -0
- package/src/wallet/eoa.ts +51 -0
- package/src/wallet/index.ts +47 -0
- package/src/wallet/keystore.ts +50 -0
- package/src/wallet/operator-keystore-crypto.ts +530 -0
- package/src/wallet/operator-session.ts +344 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux bubblewrap (`bwrap`) backend. Mirrors the macOS sandbox-exec backend
|
|
3
|
+
* for Linux operators. Wraps every tool spawn in:
|
|
4
|
+
*
|
|
5
|
+
* bwrap <profile-args...> <orig-command> <orig-args...>
|
|
6
|
+
*
|
|
7
|
+
* `bwrap` is unprivileged user-namespace sandboxing — the same primitive
|
|
8
|
+
* Flatpak / Bubblewrap / chromium use. No setuid required (kernel must have
|
|
9
|
+
* unprivileged user namespaces enabled, which is the default on every modern
|
|
10
|
+
* distro: Ubuntu 22+, Fedora, Arch, Debian 11+).
|
|
11
|
+
*
|
|
12
|
+
* Profile policy mirrors macOS seatbelt:
|
|
13
|
+
* - read-only bind of / (so commands like `cat`, `ls`, `find` work)
|
|
14
|
+
* - writable bind of agentDir + workspaceRoot
|
|
15
|
+
* - writable bind of /tmp (nebula-* dirs land there)
|
|
16
|
+
* - tmpfs overlay of credential dirs (~/.ssh, ~/.aws, ~/Library/Keychains
|
|
17
|
+
* [doesn't exist on Linux but cheap to include for portability],
|
|
18
|
+
* ~/.config/gcloud) — reads return empty
|
|
19
|
+
* - --unshare-all --share-net keeps network reachable so nebula can still
|
|
20
|
+
* hit Mantle RPC, the indexer, etc.
|
|
21
|
+
* - --die-with-parent kills the child if nebula crashes, no zombies
|
|
22
|
+
* - --new-session puts the child in its own session (Ctrl-C from nebula
|
|
23
|
+
* doesn't propagate to the inner command)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync } from 'node:fs'
|
|
27
|
+
import { credentialDirs } from './credentials'
|
|
28
|
+
import type {
|
|
29
|
+
SandboxBackend,
|
|
30
|
+
SandboxBackendOpts,
|
|
31
|
+
SandboxEnvHint,
|
|
32
|
+
SandboxSpawnRequest,
|
|
33
|
+
WrappedSpawn,
|
|
34
|
+
} from './types'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Probe order for bwrap binary. Most distros put it at /usr/bin/bwrap; some
|
|
38
|
+
* via pkg-managed systems at /usr/local/bin. First existing path wins.
|
|
39
|
+
*/
|
|
40
|
+
const BWRAP_CANDIDATES: ReadonlyArray<string> = ['/usr/bin/bwrap', '/usr/local/bin/bwrap']
|
|
41
|
+
|
|
42
|
+
function findBwrap(): string | null {
|
|
43
|
+
for (const path of BWRAP_CANDIDATES) {
|
|
44
|
+
if (existsSync(path)) return path
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the bwrap argv prefix that wraps the user command. Returns a flat
|
|
51
|
+
* argv array; the actual command + args are appended after.
|
|
52
|
+
*/
|
|
53
|
+
export function buildBwrapArgs(opts: SandboxBackendOpts): string[] {
|
|
54
|
+
const args: string[] = []
|
|
55
|
+
|
|
56
|
+
// Base filesystem: read-only bind of /. The container can read system tools
|
|
57
|
+
// (cat, ls, etc.) but cannot modify them. Override specific subdirs below.
|
|
58
|
+
args.push('--ro-bind', '/', '/')
|
|
59
|
+
|
|
60
|
+
// Writable subdirs: agentDir + workspaceRoot + /tmp (for nebula-* test dirs)
|
|
61
|
+
args.push('--bind', opts.agentDir, opts.agentDir)
|
|
62
|
+
args.push('--bind', opts.workspaceRoot, opts.workspaceRoot)
|
|
63
|
+
args.push('--bind', '/tmp', '/tmp')
|
|
64
|
+
|
|
65
|
+
// Optional extra-writable subpaths (test sandbox dirs, custom workspaces).
|
|
66
|
+
if (opts.extraWriteAllow) {
|
|
67
|
+
for (const path of opts.extraWriteAllow) {
|
|
68
|
+
args.push('--bind', path, path)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Credential blackouts: empty tmpfs overlays so reads return ENOENT-equivalent.
|
|
73
|
+
// Shared list with seatbelt-profile.ts to prevent platform drift.
|
|
74
|
+
for (const dir of credentialDirs(opts.homedir)) {
|
|
75
|
+
args.push('--tmpfs', dir)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Optional extra denies via tmpfs.
|
|
79
|
+
if (opts.extraWriteDeny) {
|
|
80
|
+
for (const path of opts.extraWriteDeny) {
|
|
81
|
+
args.push('--tmpfs', path)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// System pseudo-filesystems.
|
|
86
|
+
args.push('--proc', '/proc')
|
|
87
|
+
args.push('--dev', '/dev')
|
|
88
|
+
|
|
89
|
+
// Namespace isolation: unshare everything except network (nebula needs network
|
|
90
|
+
// for Mantle RPC, indexer, brain inference). PID namespace isolates process tree
|
|
91
|
+
// from host. UTS namespace gives the sandbox its own hostname.
|
|
92
|
+
args.push('--unshare-all', '--share-net')
|
|
93
|
+
|
|
94
|
+
// Lifetime + signal handling.
|
|
95
|
+
args.push('--die-with-parent') // child dies if nebula dies
|
|
96
|
+
args.push('--new-session') // Ctrl-C from nebula doesn't kill the child directly
|
|
97
|
+
|
|
98
|
+
return args
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class LinuxBubblewrapBackend implements SandboxBackend {
|
|
102
|
+
readonly mode = 'os' as const
|
|
103
|
+
readonly label = 'os:linux'
|
|
104
|
+
private readonly bwrapPath: string
|
|
105
|
+
private readonly bwrapArgs: string[]
|
|
106
|
+
|
|
107
|
+
constructor(opts: SandboxBackendOpts) {
|
|
108
|
+
const path = findBwrap()
|
|
109
|
+
if (!path) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`bwrap not found in ${BWRAP_CANDIDATES.join(', ')}. Linux sandbox backend requires bubblewrap (apt install bubblewrap / dnf install bubblewrap).`,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
this.bwrapPath = path
|
|
115
|
+
this.bwrapArgs = buildBwrapArgs(opts)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Test-only accessor for the bwrap argv prefix. */
|
|
119
|
+
getBwrapArgs(): readonly string[] {
|
|
120
|
+
return this.bwrapArgs
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
envHint(): SandboxEnvHint {
|
|
124
|
+
return {
|
|
125
|
+
mode: 'os',
|
|
126
|
+
label: this.label,
|
|
127
|
+
innerOs: 'linux',
|
|
128
|
+
workspaceMount: null,
|
|
129
|
+
scope:
|
|
130
|
+
'shell.run, code.execute, shell.process_start are wrapped in a bubblewrap profile; writes outside agentDir + cwd + /tmp/nebula-* are denied',
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async wrapSpawn(req: SandboxSpawnRequest): Promise<WrappedSpawn> {
|
|
135
|
+
return {
|
|
136
|
+
command: this.bwrapPath,
|
|
137
|
+
args: [...this.bwrapArgs, '--', req.command, ...req.args],
|
|
138
|
+
options: req.options,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passthrough backend. Used when `sandbox.mode = 'none'` (today's default for
|
|
3
|
+
* back-compat) or when the platform doesn't support `os` mode.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SandboxBackend, SandboxSpawnRequest, WrappedSpawn } from './types'
|
|
7
|
+
|
|
8
|
+
export class LocalBackend implements SandboxBackend {
|
|
9
|
+
readonly mode = 'none' as const
|
|
10
|
+
readonly label = 'none'
|
|
11
|
+
|
|
12
|
+
async wrapSpawn(req: SandboxSpawnRequest): Promise<WrappedSpawn> {
|
|
13
|
+
return {
|
|
14
|
+
command: req.command,
|
|
15
|
+
args: req.args,
|
|
16
|
+
options: req.options,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS sandbox-exec backend. Wraps every tool spawn in:
|
|
3
|
+
*
|
|
4
|
+
* sandbox-exec -p '<seatbelt-profile>' <orig-command> <orig-args...>
|
|
5
|
+
*
|
|
6
|
+
* `sandbox-exec` is at /usr/bin/sandbox-exec on every macOS install. The
|
|
7
|
+
* `man` page calls it "deprecated" in favour of the modern App Sandbox API,
|
|
8
|
+
* but it's still ships and is used by Apple internally; verified working on
|
|
9
|
+
* macOS 25.4.0. The deprecation is a recommendation that new GUI apps adopt
|
|
10
|
+
* App Sandbox, not a removal.
|
|
11
|
+
*
|
|
12
|
+
* The profile is built once at backend-init and reused for every spawn. The
|
|
13
|
+
* profile is passed inline via `-p` (no temp file management needed).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync } from 'node:fs'
|
|
17
|
+
import { buildSeatbeltProfile } from './seatbelt-profile'
|
|
18
|
+
import type {
|
|
19
|
+
SandboxBackend,
|
|
20
|
+
SandboxBackendOpts,
|
|
21
|
+
SandboxEnvHint,
|
|
22
|
+
SandboxSpawnRequest,
|
|
23
|
+
WrappedSpawn,
|
|
24
|
+
} from './types'
|
|
25
|
+
|
|
26
|
+
const SANDBOX_EXEC_PATH = '/usr/bin/sandbox-exec'
|
|
27
|
+
|
|
28
|
+
export class MacOSSandboxExecBackend implements SandboxBackend {
|
|
29
|
+
readonly mode = 'os' as const
|
|
30
|
+
readonly label = 'os:darwin'
|
|
31
|
+
private readonly profile: string
|
|
32
|
+
|
|
33
|
+
constructor(opts: SandboxBackendOpts) {
|
|
34
|
+
if (!existsSync(SANDBOX_EXEC_PATH)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`sandbox-exec not found at ${SANDBOX_EXEC_PATH}. macOS sandbox backend requires the system tool.`,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
this.profile = buildSeatbeltProfile({
|
|
40
|
+
agentDir: opts.agentDir,
|
|
41
|
+
workspaceRoot: opts.workspaceRoot,
|
|
42
|
+
homedir: opts.homedir,
|
|
43
|
+
extraWriteAllow: opts.extraWriteAllow,
|
|
44
|
+
extraWriteDeny: opts.extraWriteDeny,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Test-only accessor for the rendered profile. */
|
|
49
|
+
getProfile(): string {
|
|
50
|
+
return this.profile
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
envHint(): SandboxEnvHint {
|
|
54
|
+
return {
|
|
55
|
+
mode: 'os',
|
|
56
|
+
label: this.label,
|
|
57
|
+
innerOs: 'darwin',
|
|
58
|
+
workspaceMount: null,
|
|
59
|
+
scope:
|
|
60
|
+
'shell.run, code.execute, shell.process_start are wrapped in sandbox-exec; writes outside agentDir + cwd + /tmp/nebula-* are denied',
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async wrapSpawn(req: SandboxSpawnRequest): Promise<WrappedSpawn> {
|
|
65
|
+
return {
|
|
66
|
+
command: SANDBOX_EXEC_PATH,
|
|
67
|
+
args: ['-p', this.profile, req.command, ...req.args],
|
|
68
|
+
options: req.options,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS seatbelt (SBPL) profile generator. Used by MacOSSandboxExecBackend to
|
|
3
|
+
* build the `-p` argument for `sandbox-exec`.
|
|
4
|
+
*
|
|
5
|
+
* Profile policy (deny-default + targeted allows):
|
|
6
|
+
*
|
|
7
|
+
* READS: broad (allow file-read*). Reading is fine; it's writes + network
|
|
8
|
+
* exfil + process-fork-into-system that need gating. The brain's job is to
|
|
9
|
+
* help with files; we don't want it crippled on read.
|
|
10
|
+
*
|
|
11
|
+
* WRITES: deny default, allow ONLY:
|
|
12
|
+
* - agentDir (nebula state)
|
|
13
|
+
* - workspaceRoot (the cwd nebula was launched from; fs.write authorized
|
|
14
|
+
* through the modal lands here)
|
|
15
|
+
* - /tmp/nebula-* (nebula's own temp scratch — code.execute snippets, etc.)
|
|
16
|
+
* - /private/tmp/nebula-* (macOS canonical /tmp)
|
|
17
|
+
* - /var/folders (macOS user temp dir, where mkdtemp() defaults land)
|
|
18
|
+
* - any extra subpaths in `extraWriteAllow`
|
|
19
|
+
*
|
|
20
|
+
* EXPLICIT WRITE DENY (overrides allows on overlap):
|
|
21
|
+
* - $HOME/.ssh
|
|
22
|
+
* - $HOME/.aws
|
|
23
|
+
* - $HOME/Library/Keychains
|
|
24
|
+
* - $HOME/.config/gcloud
|
|
25
|
+
* - $HOME/.nebula (the broader nebula state tree — only the agent's own
|
|
26
|
+
* agentDir is allowed; brain shouldn't rewrite ~/.nebula/config.ts)
|
|
27
|
+
*
|
|
28
|
+
* NETWORK: allow* (nebula legitimately needs Mantle RPC, indexer, compute,
|
|
29
|
+
* WC relay, plus user-asked-for HTTP). Future hardening: allowlist by host.
|
|
30
|
+
*
|
|
31
|
+
* PROCESS: allow process-fork + process-exec (tools spawn child binaries).
|
|
32
|
+
* IPC: allow mach-lookup, ipc-posix-shm, signal — needed by most CLI
|
|
33
|
+
* tooling, otherwise the simplest commands fail.
|
|
34
|
+
*
|
|
35
|
+
* The seatbelt syntax is Apple's internal SBPL (Scheme-like). It's
|
|
36
|
+
* undocumented officially but stable across macOS versions; deprecated in
|
|
37
|
+
* `man sandbox-exec` but still functional and used by Apple itself for many
|
|
38
|
+
* system services.
|
|
39
|
+
*
|
|
40
|
+
* NOTE on order: in seatbelt SBPL, deny rules placed AFTER allows take
|
|
41
|
+
* precedence on overlap. So we put the denylist after the allowlist for
|
|
42
|
+
* credentials.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { credentialDirs } from './credentials'
|
|
46
|
+
|
|
47
|
+
export interface SeatbeltProfileOpts {
|
|
48
|
+
agentDir: string
|
|
49
|
+
workspaceRoot: string
|
|
50
|
+
homedir: string
|
|
51
|
+
extraWriteAllow?: string[]
|
|
52
|
+
extraWriteDeny?: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Escape a path for safe inclusion in an SBPL string literal. Seatbelt strings
|
|
57
|
+
* are double-quoted; embedded backslashes and double quotes need escaping.
|
|
58
|
+
* Newlines also break the parser. Nebula paths come from process.cwd() and
|
|
59
|
+
* homedir() so they're well-formed Unix paths in practice, but we escape
|
|
60
|
+
* defensively.
|
|
61
|
+
*/
|
|
62
|
+
function sbplEscape(p: string): string {
|
|
63
|
+
return p.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildSeatbeltProfile(opts: SeatbeltProfileOpts): string {
|
|
67
|
+
const home = sbplEscape(opts.homedir)
|
|
68
|
+
const agent = sbplEscape(opts.agentDir)
|
|
69
|
+
const workspace = sbplEscape(opts.workspaceRoot)
|
|
70
|
+
|
|
71
|
+
const allowSubpaths = [
|
|
72
|
+
`(allow file-write* (subpath "${agent}"))`,
|
|
73
|
+
`(allow file-write* (subpath "${workspace}"))`,
|
|
74
|
+
`(allow file-write* (regex #"^/tmp/nebula-"))`,
|
|
75
|
+
`(allow file-write* (regex #"^/private/tmp/nebula-"))`,
|
|
76
|
+
`(allow file-write* (subpath "/var/folders"))`,
|
|
77
|
+
`(allow file-write* (subpath "/private/var/folders"))`,
|
|
78
|
+
...(opts.extraWriteAllow ?? []).map(p => `(allow file-write* (subpath "${sbplEscape(p)}"))`),
|
|
79
|
+
].join('\n ')
|
|
80
|
+
|
|
81
|
+
const credDirs = credentialDirs(opts.homedir).map(sbplEscape)
|
|
82
|
+
const denySubpaths = [
|
|
83
|
+
...credDirs.map(p => `(deny file-write* (subpath "${p}"))`),
|
|
84
|
+
`(deny file-write* (subpath "${home}/.nebula"))`,
|
|
85
|
+
...(opts.extraWriteDeny ?? []).map(p => `(deny file-write* (subpath "${sbplEscape(p)}"))`),
|
|
86
|
+
].join('\n ')
|
|
87
|
+
|
|
88
|
+
// Read-side: allow broadly (the agent legitimately needs to read system
|
|
89
|
+
// binaries, libraries, project files, etc.) but EXPLICITLY deny credential
|
|
90
|
+
// dirs. This blocks `cat ~/.ssh/id_rsa` -- shell.run that bypasses
|
|
91
|
+
// PathGuard's tool-level checks. Network is broad; if exfil is a concern
|
|
92
|
+
// (read public file + POST somewhere), use Docker mode.
|
|
93
|
+
const denyReadSubpaths = credDirs.map(p => `(deny file-read* (subpath "${p}"))`).join('\n ')
|
|
94
|
+
|
|
95
|
+
// The agentDir is under ~/.nebula/agents/<id>/, and we deny ~/.nebula broadly,
|
|
96
|
+
// so we MUST re-allow agentDir AFTER the deny to keep nebula's own state
|
|
97
|
+
// writable. SBPL is order-sensitive: later rules win on overlap.
|
|
98
|
+
return `(version 1)
|
|
99
|
+
(deny default)
|
|
100
|
+
|
|
101
|
+
;; Process management — tools spawn binaries.
|
|
102
|
+
(allow process-fork)
|
|
103
|
+
(allow process-exec)
|
|
104
|
+
|
|
105
|
+
;; IPC + system bookkeeping that any non-trivial CLI needs.
|
|
106
|
+
(allow mach-lookup)
|
|
107
|
+
(allow mach-priv-host-port)
|
|
108
|
+
(allow mach-task-name)
|
|
109
|
+
(allow ipc-posix-shm)
|
|
110
|
+
(allow signal)
|
|
111
|
+
(allow sysctl-read)
|
|
112
|
+
(allow sysctl-write)
|
|
113
|
+
(allow system-fsctl)
|
|
114
|
+
(allow system-info)
|
|
115
|
+
(allow system-socket)
|
|
116
|
+
(allow iokit-open)
|
|
117
|
+
|
|
118
|
+
;; Network — broad. Nebula needs Mantle RPC, indexer, compute, WC relay, plus
|
|
119
|
+
;; arbitrary HTTP for browse + brain-driven fetches. Tighten via allowlist
|
|
120
|
+
;; when we have explicit host policy.
|
|
121
|
+
(allow network*)
|
|
122
|
+
|
|
123
|
+
;; Reads — broad by default so binaries/libraries/project files work.
|
|
124
|
+
(allow file-read*)
|
|
125
|
+
|
|
126
|
+
;; Explicit credential read denies (override allow file-read*).
|
|
127
|
+
${denyReadSubpaths}
|
|
128
|
+
|
|
129
|
+
;; Writes — deny default, allowlist:
|
|
130
|
+
${allowSubpaths}
|
|
131
|
+
|
|
132
|
+
;; Explicit credential + state-tree denies (override allowlist on overlap).
|
|
133
|
+
${denySubpaths}
|
|
134
|
+
|
|
135
|
+
;; Re-allow agentDir AFTER the ~/.nebula broad deny so nebula's own state is
|
|
136
|
+
;; writable. SBPL applies later rules first, so this comes last.
|
|
137
|
+
(allow file-write* (subpath "${agent}"))
|
|
138
|
+
`
|
|
139
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox abstraction for limb execution.
|
|
3
|
+
*
|
|
4
|
+
* Phase 9.5 (Apr 28 2026 incident response). Nebula's limbs run on the operator's
|
|
5
|
+
* host. Permission floors (PathGuard + dangerous-pattern modal + strict/prompt/yolo)
|
|
6
|
+
* caught the rm correctly during the v0.9.3 benchmark, but once the modal granted
|
|
7
|
+
* `s` (allow session), the command ran on the real host with full FS access. The
|
|
8
|
+
* cascade (tmux socket → daemon detach → orphan name-slot blocking) was severe.
|
|
9
|
+
*
|
|
10
|
+
* This module adds a structural layer BENEATH the permission floor: every
|
|
11
|
+
* spawn() call from a tool is routed through a `SandboxBackend` which can wrap
|
|
12
|
+
* the command in an OS sandbox before execution. Even if the permission floor
|
|
13
|
+
* is bypassed (yolo, allow-session, allow-once), the sandbox profile prevents
|
|
14
|
+
* writes outside an allowlist.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors hermes-agent's TERMINAL_ENV pattern (local | docker | modal | daytona |
|
|
17
|
+
* singularity | ssh) but starts smaller: `none` (passthrough) and `os` (macOS
|
|
18
|
+
* sandbox-exec / future Linux bubblewrap). Docker mode is a separate followup
|
|
19
|
+
* bundle.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { SpawnOptions } from 'node:child_process'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mode selector. Lives under `sandbox.mode` in `~/.nebula/config.ts`.
|
|
26
|
+
*
|
|
27
|
+
* - `none`: passthrough (today's behaviour). No sandboxing applied. Default
|
|
28
|
+
* for backward compatibility while Tier 2 stabilizes.
|
|
29
|
+
* - `os`: native OS sandbox. macOS uses sandbox-exec with a deny-default
|
|
30
|
+
* seatbelt profile. Linux uses bubblewrap (post-MVP). On unsupported
|
|
31
|
+
* platforms falls back to `none` with a startup warning.
|
|
32
|
+
* - `docker`: long-lived container per session, every spawn goes through
|
|
33
|
+
* `docker exec`. Future bundle.
|
|
34
|
+
*/
|
|
35
|
+
export type SandboxMode = 'none' | 'os' | 'docker'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Inputs the factory needs to construct a backend.
|
|
39
|
+
* - `agentDir`: write-allowed (nebula writes activity log, mcp debug, etc.).
|
|
40
|
+
* - `workspaceRoot`: write-allowed (where the operator launched nebula from;
|
|
41
|
+
* fs.write/fs.patch authorized through the modal land here).
|
|
42
|
+
* - `homedir`: used by the seatbelt profile to deny secret-bearing subdirs
|
|
43
|
+
* (`~/.ssh`, `~/.aws`, `~/Library/Keychains`, `~/.config/gcloud`).
|
|
44
|
+
* - `extraWriteAllow`: optional extra subpaths to allow writes under (test
|
|
45
|
+
* sandbox dirs, custom workspaces).
|
|
46
|
+
* - `extraWriteDeny`: optional extra subpaths to explicitly block writes.
|
|
47
|
+
*/
|
|
48
|
+
export interface SandboxBackendOpts {
|
|
49
|
+
agentDir: string
|
|
50
|
+
workspaceRoot: string
|
|
51
|
+
homedir: string
|
|
52
|
+
extraWriteAllow?: string[]
|
|
53
|
+
extraWriteDeny?: string[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One spawn request, fully described before the backend wraps it. We pass
|
|
58
|
+
* `argv` rather than the legacy `(command, options)` form because backends
|
|
59
|
+
* that prepend `sandbox-exec -p ...` need to construct an explicit argv;
|
|
60
|
+
* mixing `shell: true` with a wrapper produces confused quoting.
|
|
61
|
+
*/
|
|
62
|
+
export interface WrappedSpawn {
|
|
63
|
+
/** The binary that should actually be exec'd. May be the original, may be a wrapper. */
|
|
64
|
+
command: string
|
|
65
|
+
/** Args to pass. Wrapper backends prepend their own. */
|
|
66
|
+
args: string[]
|
|
67
|
+
/** SpawnOptions to pass through. `shell` is intentionally omitted because the wrapper builds the explicit argv. */
|
|
68
|
+
options: SpawnOptions
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Inputs the tool layer hands the backend per spawn. The backend wraps the
|
|
73
|
+
* argv (e.g. prepend `sandbox-exec -p <profile>` or rewrite as `docker exec
|
|
74
|
+
* <containerId> ...`). For shell.run-style tools, the caller MUST pass an
|
|
75
|
+
* explicit argv (`command='/bin/sh', args=['-c', userCommand]`) — the backend
|
|
76
|
+
* cannot use `shell: true` because the wrapper builds the argv itself.
|
|
77
|
+
*/
|
|
78
|
+
export interface SandboxSpawnRequest {
|
|
79
|
+
command: string
|
|
80
|
+
args: string[]
|
|
81
|
+
options: SpawnOptions
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Environment hint surfaced to the brain via the frozen prefix's # Environment
|
|
86
|
+
* block. Lets the brain skip the "run pwd + ls / + uname to figure out where
|
|
87
|
+
* I am" empirical-discovery dance — saves wasted tool calls when the brain
|
|
88
|
+
* defaults to host-style commands inside a Linux container (BSD sed, fs.read
|
|
89
|
+
* /workspace ENOENT, etc.).
|
|
90
|
+
*
|
|
91
|
+
* Each non-passthrough backend implements `envHint()` to surface its specific
|
|
92
|
+
* shape. `LocalBackend` returns null (no sandbox, no hint).
|
|
93
|
+
*/
|
|
94
|
+
export interface SandboxEnvHint {
|
|
95
|
+
mode: SandboxMode
|
|
96
|
+
label: string
|
|
97
|
+
innerOs?: 'linux' | 'darwin' | null
|
|
98
|
+
workspaceMount?: string | null
|
|
99
|
+
scope?: string | null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The backend interface. Implementations: LocalBackend (passthrough),
|
|
104
|
+
* MacOSSandboxExecBackend (sandbox-exec wrapper), LinuxBubblewrapBackend
|
|
105
|
+
* (bwrap wrapper), DockerBackend (per-session container).
|
|
106
|
+
*
|
|
107
|
+
* `wrapSpawn` is async to allow lifecycle work (e.g. DockerBackend lazy-starts
|
|
108
|
+
* the container on first call). Sync backends just `return Promise.resolve(...)`.
|
|
109
|
+
* Optional `dispose` lets backends clean up (DockerBackend kills its container).
|
|
110
|
+
* Optional `envHint` returns a brain-facing description of the sandbox shape.
|
|
111
|
+
*/
|
|
112
|
+
export interface SandboxBackend {
|
|
113
|
+
/** Backend identifier surfaced in logs / debug output. */
|
|
114
|
+
readonly mode: SandboxMode
|
|
115
|
+
/** Backend label including platform detail (e.g. 'os:darwin', 'docker:oven/bun:1'). */
|
|
116
|
+
readonly label: string
|
|
117
|
+
/**
|
|
118
|
+
* Wrap a spawn request. Returns (a Promise of) the argv that should be
|
|
119
|
+
* passed to `spawn(command, args, options)`. For `none`, returns the request
|
|
120
|
+
* unchanged. For `os`, returns a sandbox-exec wrapper. For `docker`, returns
|
|
121
|
+
* `docker exec <containerId> <orig-command>`, awaiting container start on
|
|
122
|
+
* the first call.
|
|
123
|
+
*/
|
|
124
|
+
wrapSpawn(req: SandboxSpawnRequest): Promise<WrappedSpawn>
|
|
125
|
+
/** Optional cleanup (kill long-lived containers, remove temp files). Called on nebula exit. */
|
|
126
|
+
dispose?(): Promise<void>
|
|
127
|
+
/** Optional brain-facing description of the sandbox shape. Null for passthrough. */
|
|
128
|
+
envHint?(): SandboxEnvHint | null
|
|
129
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { SkillFrontmatter, SkillRef, SkillSource } from './types'
|
|
2
|
+
export { scanSkills, parseFrontmatter, type SkillScannerOptions } from './scanner'
|
|
3
|
+
export {
|
|
4
|
+
matchTriggers,
|
|
5
|
+
matchFilePattern,
|
|
6
|
+
matchBashPattern,
|
|
7
|
+
type SkillTriggerMatch,
|
|
8
|
+
} from './triggers'
|