@forwardimpact/outpost 3.8.0 → 3.9.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.
@@ -1,32 +1,38 @@
1
1
  {
2
2
  "agents": {
3
3
  "postman": {
4
- "kb": "~/Documents/Personal",
4
+ "kb": "~/.local/share/fit/outpost/Team",
5
+ "privilege": "full",
5
6
  "schedule": { "type": "cron", "expression": "*/15 8-18 * * 1-5" },
6
7
  "enabled": true
7
8
  },
8
9
  "concierge": {
9
- "kb": "~/Documents/Personal",
10
+ "kb": "~/.local/share/fit/outpost/Team",
11
+ "privilege": "full",
10
12
  "schedule": { "type": "cron", "expression": "*/30 8-18 * * 1-5" },
11
13
  "enabled": true
12
14
  },
13
15
  "librarian": {
14
- "kb": "~/Documents/Personal",
16
+ "kb": "~/.local/share/fit/outpost/Team",
17
+ "privilege": "restricted",
15
18
  "schedule": { "type": "cron", "expression": "0 9,12,15,18 * * 1-5" },
16
19
  "enabled": true
17
20
  },
18
21
  "chief-of-staff": {
19
- "kb": "~/Documents/Personal",
22
+ "kb": "~/.local/share/fit/outpost/Team",
23
+ "privilege": "restricted",
20
24
  "schedule": { "type": "cron", "expression": "0 7,18 * * 1-5" },
21
25
  "enabled": true
22
26
  },
23
27
  "recruiter": {
24
- "kb": "~/Documents/Personal",
28
+ "kb": "~/.local/share/fit/outpost/Team",
29
+ "privilege": "restricted",
25
30
  "schedule": { "type": "cron", "expression": "0 8,12,17 * * 1-5" },
26
31
  "enabled": true
27
32
  },
28
33
  "head-hunter": {
29
- "kb": "~/Documents/Personal",
34
+ "kb": "~/.local/share/fit/outpost/Team",
35
+ "privilege": "restricted",
30
36
  "schedule": { "type": "cron", "expression": "0 9 * * 1-5" },
31
37
  "enabled": true
32
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/outpost",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "description": "Personal operations center — context from email, calendar, and knowledge assembled so preparation is continuous, not a morning scramble.",
5
5
  "homepage": "https://www.forwardimpact.team",
6
6
  "repository": {
@@ -11,6 +11,7 @@ import {
11
11
  loadManifest,
12
12
  draftSkills,
13
13
  } from "./posture.js";
14
+ import { resolvePrivilege, disclaimFor } from "./privilege.js";
14
15
  import { buildSpawnEnv } from "./spawn-env.js";
15
16
 
16
17
  /**
@@ -176,6 +177,24 @@ export class AgentRunner {
176
177
  * @param {Record<string, string>} [configEnv] - Extra env vars from config
177
178
  */
178
179
  async wake(agentName, agent, state, configEnv) {
180
+ // Resolve the mandatory privilege level before any work. A missing or
181
+ // invalid level is fail-closed: log and skip the wake — no agent process is
182
+ // spawned with a guessed privilege. The level lives in the user-only trust
183
+ // root, so a spawned agent cannot raise its own.
184
+ let level;
185
+ try {
186
+ level = resolvePrivilege(agent);
187
+ } catch (err) {
188
+ this.#log(
189
+ JSON.stringify({
190
+ event: "outpost.privilege.rejected",
191
+ agent: agentName,
192
+ error: err.message,
193
+ }),
194
+ );
195
+ return;
196
+ }
197
+
179
198
  if (!agent.kb) {
180
199
  this.#log(`Agent ${agentName}: no "kb" specified, skipping.`);
181
200
  return;
@@ -191,6 +210,13 @@ export class AgentRunner {
191
210
  const claude = await this.#findClaude();
192
211
 
193
212
  this.#log(`Waking agent: ${agentName} (kb: ${agent.kb})`);
213
+ this.#log(
214
+ JSON.stringify({
215
+ event: "outpost.privilege.resolved",
216
+ agent: agentName,
217
+ level,
218
+ }),
219
+ );
194
220
 
195
221
  const as = (state.agents[agentName] ||= {});
196
222
  as.status = "active";
@@ -226,6 +252,7 @@ export class AgentRunner {
226
252
  env,
227
253
  kbPath,
228
254
  this.#runtime,
255
+ disclaimFor(level),
229
256
  );
230
257
  this.#activeChildren.add(pid);
231
258
 
package/src/kb-manager.js CHANGED
@@ -23,6 +23,33 @@ export class KBManager {
23
23
  this.#logger = createLogger("outpost", runtime);
24
24
  }
25
25
 
26
+ /**
27
+ * Resolve a knowledge-base name to its path under the XDG data home,
28
+ * `~/.local/share/fit/outpost/<name>`. The argument is a single path segment,
29
+ * never an arbitrary filesystem path, so a provisioned KB always lands outside
30
+ * TCC-protected folders. The name is validated — not sanitised — against the
31
+ * same rule as `agent-path.js`: a name carrying `/`, `\`, `..`, NUL, or a
32
+ * leading `~` could steer the KB back inside `~/Documents`, reopening the TCC
33
+ * hole, so it is rejected rather than rewritten.
34
+ * @param {string} name - The KB name (e.g. `Team`, `personal`).
35
+ * @returns {string} Absolute path under the data home.
36
+ * @throws {Error} when `name` is empty, non-string, or an unsafe segment.
37
+ */
38
+ static kbPathForName(name) {
39
+ if (
40
+ typeof name !== "string" ||
41
+ name.length === 0 ||
42
+ name.includes("/") ||
43
+ name.includes("\\") ||
44
+ name.includes("..") ||
45
+ name.includes("\0") ||
46
+ name.startsWith("~")
47
+ ) {
48
+ throw new Error(`unsafe KB name: ${JSON.stringify(name)}`);
49
+ }
50
+ return join(homedir(), ".local/share/fit/outpost", name);
51
+ }
52
+
26
53
  /**
27
54
  * Test whether a path exists, via the async fs surface.
28
55
  * @param {string} p
package/src/outpost.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // fit-outpost Wake due agents once and exit
5
5
  // fit-outpost daemon Run continuously (poll every 60s)
6
6
  // fit-outpost wake <agent> Wake a specific agent immediately
7
- // fit-outpost init <path> Initialize a new knowledge base
7
+ // fit-outpost init [name] Initialize a knowledge base by name (default: Team)
8
8
  // fit-outpost update [path] Update KB with latest CLAUDE.md, agents and skills (defaults to current directory)
9
9
  // fit-outpost stop Gracefully stop daemon and all running agents
10
10
  // fit-outpost validate Validate agent definitions exist
@@ -58,7 +58,7 @@ function buildDefinition(version) {
58
58
  },
59
59
  {
60
60
  name: "init",
61
- args: "<path>",
61
+ args: "[name]",
62
62
  description: "Initialize a new knowledge base",
63
63
  },
64
64
  {
@@ -433,13 +433,20 @@ export async function run(runtime, version) {
433
433
  return 1;
434
434
  },
435
435
  init: async () => {
436
- if (!args[0]) {
437
- cli.usageError("missing required argument <path>");
436
+ // `init [name]` provisions a KB by name under the data home (default
437
+ // `Team`), never an arbitrary path — so the substrate cannot be
438
+ // steered back into a TCC-protected folder. An unsafe name is refused.
439
+ const name = args[0] ?? "Team";
440
+ let target;
441
+ try {
442
+ target = KBManager.kbPathForName(name);
443
+ } catch {
444
+ cli.usageError(`invalid KB name "${name}"`);
438
445
  return 2;
439
446
  }
440
447
  const tpl = await requireTemplateDir();
441
448
  if (tpl === null) return 1;
442
- const result = await kbManager.init(args[0], tpl);
449
+ const result = await kbManager.init(target, tpl);
443
450
  if (!result.ok) {
444
451
  proc.stderr.write(result.error + "\n");
445
452
  return result.code;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Privilege — resolve an agent's least-privilege execution level and map it to
3
+ * the hop-2 spawn disclaim flag.
4
+ *
5
+ * The level governs the macOS reach the daemon grants a woken agent. `full`
6
+ * keeps today's single-grant model (the child inherits `fit-outpost.app` as its
7
+ * responsible process, so Full Disk Access and Automation flow to it); a
8
+ * `restricted` agent is held responsible for itself, so those grants are not
9
+ * extended and it can reach only non-TCC-protected substrate.
10
+ *
11
+ * The level is mandatory and lives in the same user-only trust root as the
12
+ * spawn-env allow-set and the state roots, so an agent cannot raise its own
13
+ * level. Patterned on `posture.js` but with no `effective*` coercion and no
14
+ * default — a missing or unrecognised value throws.
15
+ */
16
+
17
+ /** The two privilege levels, in declaration order. */
18
+ export const PRIVILEGE_LEVELS = ["full", "restricted"];
19
+
20
+ /**
21
+ * Resolve an agent's declared privilege level. The level is mandatory: a
22
+ * missing or unrecognised value throws — there is no default.
23
+ * @param {{ privilege?: string }} agent - One agent's config.
24
+ * @returns {"full"|"restricted"} The declared level.
25
+ * @throws {Error} when `agent.privilege` is not one of {@link PRIVILEGE_LEVELS}.
26
+ */
27
+ export function resolvePrivilege(agent) {
28
+ const level = agent?.privilege;
29
+ if (!PRIVILEGE_LEVELS.includes(level)) {
30
+ throw new Error(
31
+ `invalid privilege "${level}"; expected one of ${PRIVILEGE_LEVELS.join(", ")}`,
32
+ );
33
+ }
34
+ return level;
35
+ }
36
+
37
+ /**
38
+ * Map a level to the hop-2 disclaim flag: `restricted` self-disclaims (`1`),
39
+ * `full` keeps the inherited responsible process (`0`).
40
+ * @param {"full"|"restricted"} level
41
+ * @returns {0|1}
42
+ */
43
+ export function disclaimFor(level) {
44
+ return level === "restricted" ? 1 : 0;
45
+ }
@@ -66,7 +66,7 @@ candidates, pipeline totals by stage/track, aggregate diversity, retention flags
66
66
 
67
67
  ```
68
68
  Decision: {observation and chosen action}
69
- Action: {e.g. "req-screen for John Smith against J060 forward_deployed"}
69
+ Action: {e.g. "req-screen for John Smith against J060 forward-deployed"}
70
70
  Stage: {1 | 2 | sync | erasure}
71
71
  Priority Watch: {priority at risk + one-line why, or "none"}
72
72
  ```
@@ -31,7 +31,7 @@ before deciding whether to invest interview time.
31
31
 
32
32
  - **Candidate name** — locates `Knowledge/Candidates/{Name}/brief.md`.
33
33
  - **Target role** — discipline, level, track (e.g.
34
- `software_engineering J070 forward_deployed`). If not given, infer from the
34
+ `software-engineering J070 forward-deployed`). If not given, infer from the
35
35
  candidate's `Req` field → Role file. Ask the user if it can't be inferred.
36
36
  - **Recipient** — pod lead or hiring manager the report is for.
37
37
  - **CV file** (optional) — read directly when no `screening.md` exists.
@@ -51,7 +51,7 @@ from the transcript — "seemed strong" is not evidence.
51
51
  | `emerging` | Behaviour not observed or only when prompted |
52
52
  | `developing` | Behaviour present but inconsistent or surface-level |
53
53
  | `practicing` | Behaviour consistent and natural throughout the interview |
54
- | `role_modeling` | Behaviour demonstrated at high level, influenced the room |
54
+ | `role-modeling` | Behaviour demonstrated at high level, influenced the room |
55
55
  | `exemplifying` | Behaviour exceptional, set the standard for the conversation |
56
56
 
57
57
  ## Level signals
@@ -64,7 +64,7 @@ journey.}
64
64
 
65
65
  ### Track Confirmation
66
66
 
67
- **Recommended track:** {forward_deployed / platform / either}
67
+ **Recommended track:** {forward-deployed / platform / either}
68
68
 
69
69
  {Explain track fit, referencing interview moments.}
70
70
 
@@ -10,7 +10,7 @@ Reference for `req-scan` Step 4. Save to `Knowledge/Prospects/{Name}.md`.
10
10
  **Date found:** {YYYY-MM-DD}
11
11
  **Location:** {location or "Not specified"}
12
12
  **Estimated level:** {J040–J110} (confidence: {high/medium/low})
13
- **Best track fit:** {forward_deployed / platform / either}
13
+ **Best track fit:** {forward-deployed / platform / either}
14
14
  **Match strength:** {strong / moderate}
15
15
 
16
16
  ## Profile
@@ -92,7 +92,7 @@ If no target is available, estimate one using the level heuristics in
92
92
  ```bash
93
93
  bunx fit-pathway job {discipline} {level} --track={track}
94
94
  bunx fit-pathway job {discipline} {level} --track={track} --skills
95
- bunx fit-pathway track forward_deployed
95
+ bunx fit-pathway track forward-deployed
96
96
  bunx fit-pathway track platform
97
97
  ```
98
98
 
@@ -26,7 +26,7 @@ around the screening question — is this worth an interview?}
26
26
  | Dimension | Assessment |
27
27
  | -------------- | -------------------------------------- |
28
28
  | **Level** | {estimated level and confidence} |
29
- | **Track fit** | {forward_deployed / platform / either} |
29
+ | **Track fit** | {forward-deployed / platform / either} |
30
30
  | **Discipline** | {best discipline match} |
31
31
  | **Gender** | {Woman / Man / —} |
32
32
 
@@ -54,7 +54,7 @@ Standard reference: `{discipline} {level} --track={track}`
54
54
 
55
55
  ## Track Fit Analysis
56
56
 
57
- {Paragraph explaining why the candidate fits forward_deployed,
57
+ {Paragraph explaining why the candidate fits forward-deployed,
58
58
  platform, or either. Reference specific CV evidence.}
59
59
 
60
60
  ## Screening Recommendation
@@ -186,7 +186,7 @@ either skill until it changes.
186
186
  bunx fit-pathway skill --list
187
187
  ```
188
188
 
189
- Use agent-aligned engineering standard IDs (e.g. `data_integration`,
189
+ Use agent-aligned engineering standard IDs (e.g. `data-integration`,
190
190
  `full_stack_development`, `architecture_and_design`) in the `## Skills` section
191
191
  instead of free-form tags. Flag any candidate with a CV attachment for
192
192
  `req-screen`.
@@ -15,7 +15,7 @@ Use when no Role file exists for the requisition. Filename:
15
15
  **Title:** {Full title from export}
16
16
  **Level:** {Infer from title: "Principal" → J100, "Staff" → J090, "Director" → J100 M-track, "Senior" → J070}
17
17
  **Track:** {P-track for IC roles, M-track for Director/Manager roles}
18
- **Discipline:** {Infer: "Software Engineer" → software_engineering, "Data Engineer" → data_engineering, "Data Scientist" → data_science}
18
+ **Discipline:** {Infer: "Software Engineer" → software-engineering, "Data Engineer" → data-engineering, "Data Scientist" → data-science}
19
19
  **Domain lead:** —
20
20
  **Hiring manager:** {From export metadata if available, or "—"}
21
21
  **Locations:** {Primary Location from export}