@growthub/cli 0.14.2 → 0.14.4
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/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +4 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/agent-outcomes/route.js +85 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +187 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +69 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +152 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +88 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +72 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-source/route.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +338 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +532 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +36 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +9 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +11 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +22 -165
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/ceo-agent-teams.js +211 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/ceo-bootstrap-console.js +325 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/ceo-cockpit-console.js +206 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-publish.js +179 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +89 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-registry.js +539 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +11 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-outcome-receipts.js +157 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-policy.js +402 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +69 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +10 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +203 -0
- package/package.json +2 -2
|
@@ -29,7 +29,9 @@ helpers:
|
|
|
29
29
|
- path: helpers/check-self-improving-health.sh
|
|
30
30
|
verb: check-self-improving-health
|
|
31
31
|
description: Validate self-improving workspace primitives and proposal dirs.
|
|
32
|
-
subSkills:
|
|
32
|
+
subSkills:
|
|
33
|
+
- name: governed-workspace-mutation
|
|
34
|
+
path: skills/governed-workspace-mutation/SKILL.md
|
|
33
35
|
mcpTools: []
|
|
34
36
|
---
|
|
35
37
|
|
|
@@ -44,7 +46,7 @@ Every Growthub governed Workspace is materialised from this kit. The kit ships t
|
|
|
44
46
|
3. **`templates/project.md`** — session-memory template. On `growthub starter init` and `growthub starter import-*`, the CLI copies this to `.growthub-fork/project.md` so every fork starts with an append-only editing history alongside the machine-readable `trace.jsonl`.
|
|
45
47
|
4. **`templates/self-eval.md`** — self-evaluation pattern. Describes the generate → render → evaluate → retry (≤3) loop that mirrors the Fork Sync Agent's preview → apply → trace lifecycle.
|
|
46
48
|
5. **`helpers/`** — safe shell tool layer. Scripts an agent calls via one shell invocation instead of reconstructing raw commands. Populated per fork; the baseline ships conventions only.
|
|
47
|
-
6. **`skills/`** — nested sub-skill convention. Each sub-directory is a full `SKILL.md`-addressable sub-skill that a parent agent can spawn in parallel for heavy or narrow tasks.
|
|
49
|
+
6. **`skills/`** — nested sub-skill convention. Each sub-directory is a full `SKILL.md`-addressable sub-skill that a parent agent can spawn in parallel for heavy or narrow tasks. The baseline ships [`skills/governed-workspace-mutation/SKILL.md`](./skills/governed-workspace-mutation/SKILL.md) — the runtime-verified contract card for the two canonical workspace calls (`PATCH /api/workspace`, `POST /api/workspace/sandbox-run`). **Read it before any workspace-configuration mutation or sandbox execution.**
|
|
48
50
|
|
|
49
51
|
## When to use this skill
|
|
50
52
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/workspace/agent-outcomes
|
|
3
|
+
*
|
|
4
|
+
* The governance cockpit data model (Agent Outcome Loop V1 — contract:
|
|
5
|
+
* `@growthub/api-contract/workspace-outcome::AgentOutcomesResponse`).
|
|
6
|
+
*
|
|
7
|
+
* Returns the unified receipt stream (newest first, bounded) that every
|
|
8
|
+
* mutation lane emits — direct PATCH, preflight rejections, sandbox runs,
|
|
9
|
+
* workflow publishes, helper applies — plus an always-recomputed governance
|
|
10
|
+
* summary derived from the live config:
|
|
11
|
+
*
|
|
12
|
+
* - blocked policy/gate attempts
|
|
13
|
+
* - successful publishes
|
|
14
|
+
* - drafts waiting for a test
|
|
15
|
+
* - drafts tested but not published
|
|
16
|
+
* - live workflow rows whose last run failed
|
|
17
|
+
* - live workflow rows with no recorded run at all
|
|
18
|
+
* - helper applies
|
|
19
|
+
*
|
|
20
|
+
* Read-only. This is how an operator manages a workspace full of agents
|
|
21
|
+
* without reading logs, and how the next agent inherits context: read the
|
|
22
|
+
* stream, cite receiptIds, continue from `nextActions` / `rollbackRef`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { NextResponse } from "next/server";
|
|
26
|
+
import { readWorkspaceConfig } from "@/lib/workspace-config";
|
|
27
|
+
import { AGENT_OUTCOMES_SOURCE_ID, readOutcomeReceipts } from "@/lib/workspace-outcome-receipts";
|
|
28
|
+
|
|
29
|
+
function hasValue(v) {
|
|
30
|
+
return Boolean(String(v ?? "").trim());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deriveRowCounters(workspaceConfig) {
|
|
34
|
+
const counters = {
|
|
35
|
+
draftsAwaitingTest: 0,
|
|
36
|
+
draftsTestedNotPublished: 0,
|
|
37
|
+
liveRowsWithFailedLastRun: 0,
|
|
38
|
+
liveRowsWithoutProof: 0
|
|
39
|
+
};
|
|
40
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
41
|
+
for (const object of objects) {
|
|
42
|
+
if (object?.objectType !== "sandbox-environment") continue;
|
|
43
|
+
for (const row of Array.isArray(object.rows) ? object.rows : []) {
|
|
44
|
+
const hasDraft = hasValue(row?.orchestrationDraftConfig) || hasValue(row?.orchestrationDraftGraph);
|
|
45
|
+
const attested = row?.orchestrationDraftTestPassed === true
|
|
46
|
+
|| String(row?.orchestrationDraftTestPassed ?? "") === "true";
|
|
47
|
+
if (hasDraft && !attested) counters.draftsAwaitingTest += 1;
|
|
48
|
+
if (hasDraft && attested) counters.draftsTestedNotPublished += 1;
|
|
49
|
+
const isLive = String(row?.lifecycleStatus ?? "").trim().toLowerCase() === "live";
|
|
50
|
+
if (isLive && String(row?.status ?? "") === "failed") counters.liveRowsWithFailedLastRun += 1;
|
|
51
|
+
if (isLive && !hasValue(row?.lastRunId)) counters.liveRowsWithoutProof += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return counters;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function GET(request) {
|
|
58
|
+
const { searchParams } = new URL(request.url);
|
|
59
|
+
const limitRaw = Number(searchParams.get("limit") || 100);
|
|
60
|
+
const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(1, limitRaw), 200) : 100;
|
|
61
|
+
|
|
62
|
+
const receipts = await readOutcomeReceipts(limit);
|
|
63
|
+
let workspaceConfig = null;
|
|
64
|
+
try {
|
|
65
|
+
workspaceConfig = await readWorkspaceConfig();
|
|
66
|
+
} catch {
|
|
67
|
+
workspaceConfig = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const summary = {
|
|
71
|
+
blockedAttempts: receipts.filter((r) => r?.outcomeStatus === "blocked").length,
|
|
72
|
+
publishes: receipts.filter((r) => r?.kind === "workflow-publish" && r?.outcomeStatus === "published").length,
|
|
73
|
+
helperApplies: receipts.filter((r) => r?.kind === "helper-apply").length,
|
|
74
|
+
...deriveRowCounters(workspaceConfig)
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return NextResponse.json({
|
|
78
|
+
ok: true,
|
|
79
|
+
sourceId: AGENT_OUTCOMES_SOURCE_ID,
|
|
80
|
+
receipts,
|
|
81
|
+
summary
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { GET };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/workspace/apps
|
|
3
|
+
*
|
|
4
|
+
* Governed Application Control Plane V1 — the fleet read surface (contract:
|
|
5
|
+
* `@growthub/api-contract/workspace-apps::WorkspaceAppsResponse`).
|
|
6
|
+
*
|
|
7
|
+
* Returns, read-only and secret-free:
|
|
8
|
+
*
|
|
9
|
+
* - `apps[]` — every application registered as a governed row of the
|
|
10
|
+
* `workspace-app-registry` Data Model object: resolved
|
|
11
|
+
* links (dashboards / workflows / data sources / APIs),
|
|
12
|
+
* health rollup with computed blockers, the single next
|
|
13
|
+
* action (with an href into the real surface), and the
|
|
14
|
+
* machine-readable agent assignment packet.
|
|
15
|
+
* - `detected[]` — app surfaces detected on the artifact's own filesystem
|
|
16
|
+
* (same probe heuristics as the CLI's `workspace surface
|
|
17
|
+
* list`, bridged into the runtime so registration never
|
|
18
|
+
* requires a separate tool). Detection is advisory: an
|
|
19
|
+
* app becomes GOVERNED only when a human/agent registers
|
|
20
|
+
* it as a row via the normal PATCH lane.
|
|
21
|
+
* - `lens` — the Fleet lens state (same deriver the Workspace Lens
|
|
22
|
+
* panel renders), so humans and agents read one truth.
|
|
23
|
+
* - `summary` — fleet counters.
|
|
24
|
+
*
|
|
25
|
+
* Authority invariants: GET only; mutations flow through the existing
|
|
26
|
+
* governed routes; this route never throws on partial/absent config.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { NextResponse } from "next/server";
|
|
30
|
+
import fsSync from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
import {
|
|
33
|
+
describePersistenceMode,
|
|
34
|
+
readWorkspaceConfig,
|
|
35
|
+
readWorkspaceSourceRecords
|
|
36
|
+
} from "@/lib/workspace-config";
|
|
37
|
+
import { readAdapterConfig } from "@/lib/adapters/env";
|
|
38
|
+
import {
|
|
39
|
+
APP_REGISTRY_OBJECT_ID,
|
|
40
|
+
buildAppAssignmentPacket,
|
|
41
|
+
deriveAppHealth,
|
|
42
|
+
deriveAppNextAction,
|
|
43
|
+
listAppSurfaceRows,
|
|
44
|
+
summarizeFleet
|
|
45
|
+
} from "@/lib/workspace-app-registry";
|
|
46
|
+
import {
|
|
47
|
+
deriveDeployLensState,
|
|
48
|
+
deriveFleetLensState,
|
|
49
|
+
deriveRuntimeDurability
|
|
50
|
+
} from "@/lib/workspace-activation";
|
|
51
|
+
|
|
52
|
+
/** Same safe runtime descriptor the swarm-condition route assembles. */
|
|
53
|
+
function safeRuntime(warnings) {
|
|
54
|
+
const runtime = { persistenceMode: "", persistenceAdapter: null, allowFsWrite: false, nangoConfigured: false, deploy: {} };
|
|
55
|
+
try {
|
|
56
|
+
const persistence = describePersistenceMode();
|
|
57
|
+
runtime.persistenceMode = persistence.mode;
|
|
58
|
+
runtime.allowFsWrite = persistence.mode === "filesystem" && persistence.canSave === true;
|
|
59
|
+
const adapter = readAdapterConfig();
|
|
60
|
+
runtime.persistenceAdapter = persistence.mode === "database" ? (adapter.dataAdapter || null) : null;
|
|
61
|
+
runtime.nangoConfigured = Boolean(adapter?.nango?.hasSecretKey);
|
|
62
|
+
runtime.deploy = { target: adapter.deployTarget || "" };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
warnings.push(`Failed to read runtime descriptor: ${error?.message || "unknown error"}`);
|
|
65
|
+
}
|
|
66
|
+
return runtime;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Mirrors the CLI probe (cli/src/commands/workspace-surface.ts) so detection
|
|
70
|
+
// lives inside the artifact too — the bridge roadmap Item 4 called for.
|
|
71
|
+
const KNOWN_APP_DIRS = ["apps/workspace", "apps/agency-portal", "apps/portal", "studio", "app", "src"];
|
|
72
|
+
|
|
73
|
+
function detectFramework(absPath) {
|
|
74
|
+
try {
|
|
75
|
+
const entries = fsSync.readdirSync(absPath);
|
|
76
|
+
if (entries.some((e) => e.startsWith("next.config."))) return "nextjs";
|
|
77
|
+
if (entries.some((e) => e.startsWith("vite.config."))) return "vite";
|
|
78
|
+
} catch {
|
|
79
|
+
/* unreadable */
|
|
80
|
+
}
|
|
81
|
+
return "unknown";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function looksLikeAppSurface(absPath) {
|
|
85
|
+
try {
|
|
86
|
+
const has = (rel) => fsSync.existsSync(path.join(absPath, rel));
|
|
87
|
+
return has("package.json") || has("index.html") || has("app") || has("pages") || has("src");
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Read-only filesystem probe of the artifact root. Never throws. */
|
|
94
|
+
function detectAppSurfaces(warnings) {
|
|
95
|
+
const detected = [];
|
|
96
|
+
try {
|
|
97
|
+
// The workspace app runs from <artifact>/apps/workspace.
|
|
98
|
+
const artifactRoot = path.resolve(process.cwd(), "..", "..");
|
|
99
|
+
const candidates = new Set(KNOWN_APP_DIRS);
|
|
100
|
+
const appsDir = path.join(artifactRoot, "apps");
|
|
101
|
+
if (fsSync.existsSync(appsDir)) {
|
|
102
|
+
for (const entry of fsSync.readdirSync(appsDir, { withFileTypes: true })) {
|
|
103
|
+
if (entry.isDirectory()) candidates.add(`apps/${entry.name}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const rel of Array.from(candidates).sort()) {
|
|
107
|
+
const abs = path.join(artifactRoot, rel);
|
|
108
|
+
if (!fsSync.existsSync(abs) || !fsSync.statSync(abs).isDirectory()) continue;
|
|
109
|
+
if (!looksLikeAppSurface(abs)) continue;
|
|
110
|
+
let packageName;
|
|
111
|
+
try {
|
|
112
|
+
packageName = JSON.parse(fsSync.readFileSync(path.join(abs, "package.json"), "utf8")).name;
|
|
113
|
+
} catch {
|
|
114
|
+
packageName = undefined;
|
|
115
|
+
}
|
|
116
|
+
detected.push({
|
|
117
|
+
name: rel.split("/").pop(),
|
|
118
|
+
relPath: rel,
|
|
119
|
+
framework: detectFramework(abs),
|
|
120
|
+
hasEnvExample: fsSync.existsSync(path.join(abs, ".env.example")),
|
|
121
|
+
hasVercelJson: fsSync.existsSync(path.join(abs, "vercel.json")),
|
|
122
|
+
hasGrowthubConfig: fsSync.existsSync(path.join(abs, "growthub.config.json")),
|
|
123
|
+
...(packageName ? { packageName } : {})
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
warnings.push(`Surface detection failed: ${error?.message || "unknown error"}`);
|
|
128
|
+
}
|
|
129
|
+
return detected;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function GET() {
|
|
133
|
+
const warnings = [];
|
|
134
|
+
|
|
135
|
+
let workspaceConfig = {};
|
|
136
|
+
try {
|
|
137
|
+
workspaceConfig = (await readWorkspaceConfig()) || {};
|
|
138
|
+
} catch (error) {
|
|
139
|
+
warnings.push(`Failed to read workspace config: ${error?.message || "unknown error"}`);
|
|
140
|
+
}
|
|
141
|
+
let workspaceSourceRecords = {};
|
|
142
|
+
try {
|
|
143
|
+
workspaceSourceRecords = (await readWorkspaceSourceRecords()) || {};
|
|
144
|
+
} catch {
|
|
145
|
+
workspaceSourceRecords = {};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const runtime = safeRuntime(warnings);
|
|
149
|
+
const metadataGraph = { runtime };
|
|
150
|
+
const lensInput = { workspaceConfig, workspaceSourceRecords, metadataGraph };
|
|
151
|
+
const dur = deriveRuntimeDurability(metadataGraph);
|
|
152
|
+
const runtimeFlags = {
|
|
153
|
+
durable: dur.durable,
|
|
154
|
+
readOnly: dur.readOnly,
|
|
155
|
+
deployReady: deriveDeployLensState(lensInput).complete
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const apps = listAppSurfaceRows(workspaceConfig).map((row) => {
|
|
159
|
+
const health = deriveAppHealth(workspaceConfig, workspaceSourceRecords, row, runtimeFlags);
|
|
160
|
+
return {
|
|
161
|
+
appId: String(row.appId || row.Name || "").trim(),
|
|
162
|
+
name: String(row.Name || "").trim(),
|
|
163
|
+
surfacePath: String(row.surfacePath || "").trim() || null,
|
|
164
|
+
framework: String(row.framework || "").trim() || null,
|
|
165
|
+
owner: String(row.owner || "").trim() || null,
|
|
166
|
+
environment: String(row.environment || "").trim() || null,
|
|
167
|
+
deployTarget: String(row.deployTarget || "").trim() || null,
|
|
168
|
+
registryHref: `/data-model?object=${APP_REGISTRY_OBJECT_ID}`,
|
|
169
|
+
health: { status: health.status, blockers: health.blockers, linkedCount: health.linkedCount },
|
|
170
|
+
links: health.links,
|
|
171
|
+
nextAction: deriveAppNextAction(row, health),
|
|
172
|
+
assignment: buildAppAssignmentPacket(workspaceConfig, workspaceSourceRecords, row, runtimeFlags)
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return NextResponse.json({
|
|
177
|
+
ok: true,
|
|
178
|
+
registryObjectId: APP_REGISTRY_OBJECT_ID,
|
|
179
|
+
apps,
|
|
180
|
+
detected: detectAppSurfaces(warnings),
|
|
181
|
+
lens: deriveFleetLensState(lensInput),
|
|
182
|
+
summary: summarizeFleet(apps),
|
|
183
|
+
...(warnings.length ? { warnings } : {})
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { GET };
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
writeWorkspaceSourceRecords,
|
|
36
36
|
describePersistenceMode,
|
|
37
37
|
} from "@/lib/workspace-config";
|
|
38
|
+
import { appendOutcomeReceipt } from "@/lib/workspace-outcome-receipts";
|
|
39
|
+
import { buildAppScopeViolation } from "@/lib/workspace-app-registry";
|
|
38
40
|
import {
|
|
39
41
|
applyProposalToConfig,
|
|
40
42
|
validateProposalForApply,
|
|
@@ -54,6 +56,10 @@ import {
|
|
|
54
56
|
findSwarmRunRows,
|
|
55
57
|
summarizeSwarmRunProposal,
|
|
56
58
|
} from "@/lib/workspace-swarm-proposal";
|
|
59
|
+
import {
|
|
60
|
+
CEO_BOOTSTRAP_COMPLETE_PROPOSAL_TYPE,
|
|
61
|
+
buildCeoBootstrapCompletion,
|
|
62
|
+
} from "@/lib/ceo-bootstrap-console";
|
|
57
63
|
|
|
58
64
|
const HELPER_APPLY_SOURCE_KEY = "helper:apply:receipts";
|
|
59
65
|
|
|
@@ -204,6 +210,23 @@ function normalizeSwarmRunProposal(proposal, workspaceConfig) {
|
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
async function POST(request) {
|
|
213
|
+
// helper/apply is the OPERATOR lane (human-reviewed proposals). It is not
|
|
214
|
+
// available under an app scope — app-scoped agents use preflight + scoped
|
|
215
|
+
// PATCH + sandbox-run + workflow/publish. Advertised truthfully in
|
|
216
|
+
// AppAssignmentPacket.operatorOnlyRoutes.
|
|
217
|
+
const scopedHeader = request.headers.get("x-growthub-app-scope");
|
|
218
|
+
if (scopedHeader && scopedHeader.trim()) {
|
|
219
|
+
const violation = buildAppScopeViolation(scopedHeader.trim(), "route_operator_only",
|
|
220
|
+
["POST /api/workspace/helper/apply"],
|
|
221
|
+
"helper/apply is the human-reviewed operator lane; app-scoped agents mutate via preflight + scoped PATCH", null);
|
|
222
|
+
await appendOutcomeReceipt({
|
|
223
|
+
kind: "helper-apply", lane: "governed-proposal", outcomeStatus: "blocked",
|
|
224
|
+
appId: scopedHeader.trim(),
|
|
225
|
+
summary: "helper/apply rejected (422 app scope): operator-only lane",
|
|
226
|
+
nextActions: violation.repairPlan
|
|
227
|
+
});
|
|
228
|
+
return NextResponse.json(violation, { status: 422 });
|
|
229
|
+
}
|
|
207
230
|
let body;
|
|
208
231
|
try {
|
|
209
232
|
body = await request.json();
|
|
@@ -248,8 +271,14 @@ async function POST(request) {
|
|
|
248
271
|
);
|
|
249
272
|
const resolverProposals = normalizedProposals.filter((p) => p?.type === RESOLVER_PROPOSAL_TYPE);
|
|
250
273
|
const swarmProposals = normalizedProposals.filter((p) => SWARM_PROPOSAL_TYPES.includes(p?.type));
|
|
274
|
+
const ceoBootstrapProposals = normalizedProposals.filter(
|
|
275
|
+
(p) => p?.type === CEO_BOOTSTRAP_COMPLETE_PROPOSAL_TYPE
|
|
276
|
+
);
|
|
251
277
|
const configProposals = normalizedProposals.filter(
|
|
252
|
-
(p) =>
|
|
278
|
+
(p) =>
|
|
279
|
+
p?.type !== RESOLVER_PROPOSAL_TYPE &&
|
|
280
|
+
!SWARM_PROPOSAL_TYPES.includes(p?.type) &&
|
|
281
|
+
p?.type !== CEO_BOOTSTRAP_COMPLETE_PROPOSAL_TYPE
|
|
253
282
|
);
|
|
254
283
|
|
|
255
284
|
for (const proposal of resolverProposals) {
|
|
@@ -291,6 +320,28 @@ async function POST(request) {
|
|
|
291
320
|
});
|
|
292
321
|
}
|
|
293
322
|
|
|
323
|
+
// CEO bootstrap lane — stamps the "CEO setup complete" marker onto the
|
|
324
|
+
// well-known workspace-helper sandbox row in the EXISTING dataModel patch
|
|
325
|
+
// field. The builder refuses unless the loop is config-provably done (a
|
|
326
|
+
// ready swarm with a completed run), so completion is evidence, not a
|
|
327
|
+
// client assertion. No new object, no execution.
|
|
328
|
+
for (const proposal of ceoBootstrapProposals) {
|
|
329
|
+
const result = buildCeoBootstrapCompletion({
|
|
330
|
+
workspaceConfig: workingConfig,
|
|
331
|
+
completedAt: appliedAt,
|
|
332
|
+
completedBy: reviewedBy || "user",
|
|
333
|
+
});
|
|
334
|
+
if (!result.ok) {
|
|
335
|
+
skipped.push({ proposal, reason: result.error || "CEO bootstrap not ready to complete" });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
workingConfig = result.config;
|
|
339
|
+
applied.push({
|
|
340
|
+
...buildApplyReceipt({ ...proposal, affectedField: "dataModel" }, appliedAt, reviewedBy, sessionId),
|
|
341
|
+
summary: "CEO setup marked complete",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
294
345
|
for (const proposal of configProposals) {
|
|
295
346
|
if (
|
|
296
347
|
!proposal ||
|
|
@@ -516,6 +567,23 @@ async function POST(request) {
|
|
|
516
567
|
if (row && Array.isArray(row.messages)) messagesAfterApply = row.messages;
|
|
517
568
|
}
|
|
518
569
|
|
|
570
|
+
// Agent Outcome Loop V1: the helper lane is GOVERNED-PROPOSAL — privileged
|
|
571
|
+
// (human-reviewed, server-built payloads), never an unlabelled bypass. It
|
|
572
|
+
// emits the same canonical receipt class as every other mutation lane, so
|
|
573
|
+
// the cockpit sees one stream.
|
|
574
|
+
await appendOutcomeReceipt({
|
|
575
|
+
kind: "helper-apply",
|
|
576
|
+
lane: "governed-proposal",
|
|
577
|
+
outcomeStatus: applied.length > 0 ? "published" : "failed",
|
|
578
|
+
actor: reviewedBy || "operator",
|
|
579
|
+
changedFields: Array.from(new Set(mutatingApplied.map((r) => r.affectedField))),
|
|
580
|
+
objectRefs: threadId ? [{ objectId: "helper-threads", rowName: threadId }] : undefined,
|
|
581
|
+
summary: `helper apply: ${applied.length} applied (${Array.from(new Set(applied.map((r) => r.type))).join(", ") || "none"}), ${skipped.length} skipped`,
|
|
582
|
+
...(skipped.length > 0
|
|
583
|
+
? { nextActions: skipped.slice(0, 3).map((s) => `skipped ${s.proposal?.type || "proposal"}: ${s.reason}`) }
|
|
584
|
+
: {})
|
|
585
|
+
});
|
|
586
|
+
|
|
519
587
|
return NextResponse.json({
|
|
520
588
|
ok: true,
|
|
521
589
|
threadId,
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/workspace/patch/preflight
|
|
3
|
+
*
|
|
4
|
+
* Dry-run for `PATCH /api/workspace`. Takes the exact body you intend to
|
|
5
|
+
* PATCH and returns the structured result of every gate the real PATCH will
|
|
6
|
+
* apply — allowlist + mutation policy (workspace-patch-policy.js) + full
|
|
7
|
+
* schema validation of the merged config (workspace-schema.js) — without
|
|
8
|
+
* ever writing.
|
|
9
|
+
*
|
|
10
|
+
* Always responds 200; `ok` is the verdict. Agents should preflight any
|
|
11
|
+
* non-trivial patch (especially dataModel mutations) and fix every reason
|
|
12
|
+
* before issuing the real PATCH.
|
|
13
|
+
*
|
|
14
|
+
* Response:
|
|
15
|
+
* {
|
|
16
|
+
* ok: boolean,
|
|
17
|
+
* allowed: string[], // the permanent PATCH allowlist
|
|
18
|
+
* policy: { ok, violations: [{ code, path, message }] },
|
|
19
|
+
* schema: { ok, errors: string[] },
|
|
20
|
+
* persistence: { mode, canSave, guidance } // would the write even land?
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { NextResponse } from "next/server";
|
|
25
|
+
import {
|
|
26
|
+
applyWorkspaceConfigPatch,
|
|
27
|
+
describePersistenceMode,
|
|
28
|
+
readWorkspaceConfig,
|
|
29
|
+
validateWorkspaceConfig
|
|
30
|
+
} from "@/lib/workspace-config";
|
|
31
|
+
import {
|
|
32
|
+
WORKSPACE_PATCH_ALLOWED_FIELDS,
|
|
33
|
+
evaluateWorkspacePatchPolicy,
|
|
34
|
+
repairPlanForViolations
|
|
35
|
+
} from "@/lib/workspace-patch-policy";
|
|
36
|
+
import { evaluateAppScope, requireAppScope } from "@/lib/workspace-app-registry";
|
|
37
|
+
import { appendOutcomeReceipt } from "@/lib/workspace-outcome-receipts";
|
|
38
|
+
|
|
39
|
+
async function POST(request) {
|
|
40
|
+
let patch;
|
|
41
|
+
try {
|
|
42
|
+
patch = await request.json();
|
|
43
|
+
} catch {
|
|
44
|
+
return NextResponse.json({
|
|
45
|
+
ok: false,
|
|
46
|
+
allowed: WORKSPACE_PATCH_ALLOWED_FIELDS,
|
|
47
|
+
policy: { ok: false, violations: [{ code: "invalid_body", path: "", message: "invalid json body" }] },
|
|
48
|
+
schema: { ok: false, errors: [] },
|
|
49
|
+
persistence: null
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let currentConfig = null;
|
|
54
|
+
try {
|
|
55
|
+
currentConfig = await readWorkspaceConfig();
|
|
56
|
+
} catch {
|
|
57
|
+
currentConfig = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const policy = evaluateWorkspacePatchPolicy(currentConfig, patch);
|
|
61
|
+
|
|
62
|
+
// Schema check uses writeWorkspaceConfig's EXACT merge step
|
|
63
|
+
// (applyWorkspaceConfigPatch) — canvas patches merge over the current
|
|
64
|
+
// canvas with layout/bindings preservation and null-deletes, never a
|
|
65
|
+
// top-level replacement — so preflight can never disagree with the real
|
|
66
|
+
// PATCH about the merged result. Skipped when the body is not a plain
|
|
67
|
+
// object (policy already reports that).
|
|
68
|
+
let schema = { ok: true, errors: [] };
|
|
69
|
+
if (patch && typeof patch === "object" && !Array.isArray(patch)) {
|
|
70
|
+
const sanitized = {};
|
|
71
|
+
for (const key of WORKSPACE_PATCH_ALLOWED_FIELDS) {
|
|
72
|
+
if (Object.prototype.hasOwnProperty.call(patch, key)) sanitized[key] = patch[key];
|
|
73
|
+
}
|
|
74
|
+
const merged = applyWorkspaceConfigPatch(currentConfig || {}, sanitized);
|
|
75
|
+
try {
|
|
76
|
+
validateWorkspaceConfig({
|
|
77
|
+
dashboards: merged.dashboards,
|
|
78
|
+
widgetTypes: merged.widgetTypes,
|
|
79
|
+
canvas: merged.canvas,
|
|
80
|
+
dataModel: merged.dataModel
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
schema = {
|
|
84
|
+
ok: false,
|
|
85
|
+
errors: Array.isArray(error?.details) ? error.details : [error?.message || "invalid workspace config"]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
schema = { ok: false, errors: ["patch must be a plain object"] };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Preflight mirrors PATCH exactly, INCLUDING the app-scope verdict
|
|
93
|
+
// (OpenClaw: preflight is the single source of truth — if appScopeVerdict
|
|
94
|
+
// is not allowed, the real PATCH with the same header will 422 the same way).
|
|
95
|
+
const scope = requireAppScope(request, currentConfig || {});
|
|
96
|
+
let appScopeVerdict = null;
|
|
97
|
+
if (scope.scoped) {
|
|
98
|
+
if (scope.violation) {
|
|
99
|
+
appScopeVerdict = { allowed: false, appScope: scope.violation.appScope, violations: [scope.violation] };
|
|
100
|
+
} else {
|
|
101
|
+
const verdict = evaluateAppScope(currentConfig || {}, patch, scope.appId);
|
|
102
|
+
appScopeVerdict = { allowed: verdict.ok, appScope: scope.appId, violations: verdict.violations };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const persistence = describePersistenceMode();
|
|
107
|
+
const ok = policy.ok && schema.ok && (appScopeVerdict ? appScopeVerdict.allowed : true);
|
|
108
|
+
const repairPlan = repairPlanForViolations(policy.violations);
|
|
109
|
+
// The single next governed call, when derivable from the verdicts.
|
|
110
|
+
let safeNextStep;
|
|
111
|
+
if (ok) {
|
|
112
|
+
safeNextStep = "PATCH /api/workspace with this exact body";
|
|
113
|
+
} else if (policy.violations.some((v) => v.code === "live_workflow_field" || v.code === "live_publish_via_patch")) {
|
|
114
|
+
safeNextStep =
|
|
115
|
+
"Save the graph as a draft field, prove it with POST /api/workspace/sandbox-run {useDraft:true}, then POST /api/workspace/workflow/publish";
|
|
116
|
+
} else if (repairPlan.length > 0) {
|
|
117
|
+
safeNextStep = repairPlan[0];
|
|
118
|
+
} else if (!schema.ok) {
|
|
119
|
+
safeNextStep = "Fix the schema errors against apps/workspace/lib/workspace-schema.js, then preflight again";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!ok) {
|
|
123
|
+
// Failed attempts are governance signal — visible in the cockpit stream.
|
|
124
|
+
await appendOutcomeReceipt({
|
|
125
|
+
kind: "patch-preflight",
|
|
126
|
+
lane: "untrusted-direct",
|
|
127
|
+
outcomeStatus: "blocked",
|
|
128
|
+
changedFields: patch && typeof patch === "object" && !Array.isArray(patch) ? Object.keys(patch) : [],
|
|
129
|
+
policyVerdict: { ok: policy.ok, violationCodes: policy.violations.map((v) => v.code) },
|
|
130
|
+
schemaVerdict: { ok: schema.ok, errorCount: schema.errors.length },
|
|
131
|
+
summary: `preflight blocked: ${[...policy.violations.map((v) => v.code), ...(schema.ok ? [] : ["schema"])].join(", ")}`,
|
|
132
|
+
nextActions: repairPlan.length ? repairPlan : (safeNextStep ? [safeNextStep] : [])
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return NextResponse.json({
|
|
137
|
+
ok,
|
|
138
|
+
allowed: WORKSPACE_PATCH_ALLOWED_FIELDS,
|
|
139
|
+
policy,
|
|
140
|
+
schema,
|
|
141
|
+
repairPlan,
|
|
142
|
+
...(appScopeVerdict ? { appScopeVerdict } : {}),
|
|
143
|
+
...(safeNextStep ? { safeNextStep } : {}),
|
|
144
|
+
persistence: {
|
|
145
|
+
mode: persistence.mode,
|
|
146
|
+
canSave: persistence.canSave,
|
|
147
|
+
guidance: persistence.guidance
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { POST };
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import { NextResponse } from "next/server";
|
|
34
|
+
import { requireAppScope, checkScopedSourceAccess } from "@/lib/workspace-app-registry";
|
|
35
|
+
import { appendOutcomeReceipt } from "@/lib/workspace-outcome-receipts";
|
|
34
36
|
import { readAdapterConfig } from "@/lib/adapters/env";
|
|
35
37
|
import { listGovernedWorkspaceIntegrations } from "@/lib/adapters/integrations";
|
|
36
38
|
import { readWorkspaceConfig, writeWorkspaceSourceRecords } from "@/lib/workspace-config";
|
|
@@ -50,6 +52,25 @@ async function POST(request) {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
const { sourceIds } = body;
|
|
55
|
+
// App-scope gate: a scoped agent may only refresh sources referenced on
|
|
56
|
+
// its registry row (dataSourceIds / derived sourceIds).
|
|
57
|
+
{
|
|
58
|
+
let cfgForScope = {};
|
|
59
|
+
try { cfgForScope = await readWorkspaceConfig(); } catch { cfgForScope = {}; }
|
|
60
|
+
const scope = requireAppScope(request, cfgForScope);
|
|
61
|
+
if (scope.scoped) {
|
|
62
|
+
const violation = scope.violation || checkScopedSourceAccess(scope.context, sourceIds);
|
|
63
|
+
if (violation) {
|
|
64
|
+
await appendOutcomeReceipt({
|
|
65
|
+
kind: "agent-outcome", lane: "untrusted-direct", outcomeStatus: "blocked",
|
|
66
|
+
appId: violation.appScope || scope.appId,
|
|
67
|
+
summary: `refresh-sources rejected (422 app scope): ${violation.violationType}`,
|
|
68
|
+
nextActions: violation.repairPlan
|
|
69
|
+
});
|
|
70
|
+
return NextResponse.json(violation, { status: 422 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
53
74
|
if (!Array.isArray(sourceIds) || sourceIds.length === 0) {
|
|
54
75
|
return NextResponse.json({ error: "sourceIds must be a non-empty array" }, { status: 400 });
|
|
55
76
|
}
|