@haaaiawd/second-nature 0.1.17 → 0.1.18
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/index.js +855 -855
- package/openclaw.plugin.json +29 -29
- package/package.json +52 -52
- package/runtime/cli/commands/index.d.ts +14 -14
- package/runtime/cli/commands/index.js +193 -193
- package/runtime/cli/explain/explain-surface-subject.d.ts +8 -8
- package/runtime/cli/explain/explain-surface-subject.js +9 -9
- package/runtime/cli/explain/format-explanation.d.ts +12 -12
- package/runtime/cli/explain/format-explanation.js +12 -12
- package/runtime/cli/explain/resolve-subject.js +41 -41
- package/runtime/cli/host-capability/classify-delivery.d.ts +14 -14
- package/runtime/cli/host-capability/classify-delivery.js +20 -20
- package/runtime/cli/host-capability/probe-host-capability.d.ts +2 -2
- package/runtime/cli/host-capability/probe-host-capability.js +58 -58
- package/runtime/cli/host-capability/record-host-capability.d.ts +6 -6
- package/runtime/cli/host-capability/record-host-capability.js +14 -14
- package/runtime/cli/host-capability/types.d.ts +71 -71
- package/runtime/cli/host-capability/types.js +6 -6
- package/runtime/cli/host-smoke/run-host-smoke.d.ts +2 -2
- package/runtime/cli/host-smoke/run-host-smoke.js +40 -40
- package/runtime/cli/host-smoke/types.d.ts +35 -35
- package/runtime/cli/host-smoke/types.js +6 -6
- package/runtime/cli/index.js +58 -54
- package/runtime/cli/ops/heartbeat-surface.d.ts +38 -35
- package/runtime/cli/ops/heartbeat-surface.js +73 -71
- package/runtime/cli/ops/ops-router.d.ts +19 -16
- package/runtime/cli/ops/ops-router.js +89 -87
- package/runtime/cli/ops/show-operator-fallback.d.ts +13 -13
- package/runtime/cli/ops/show-operator-fallback.js +22 -22
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +19 -10
- package/runtime/cli/ops/workspace-heartbeat-runner.js +39 -26
- package/runtime/cli/read-models/index.d.ts +29 -29
- package/runtime/cli/read-models/index.js +256 -256
- package/runtime/cli/read-models/operator-explain-map.d.ts +6 -6
- package/runtime/cli/read-models/operator-explain-map.js +10 -10
- package/runtime/cli/read-models/types.d.ts +79 -79
- package/runtime/cli/runtime/runtime-artifact-boundary.d.ts +28 -28
- package/runtime/cli/runtime/runtime-artifact-boundary.js +94 -94
- package/runtime/connectors/base/contract.d.ts +87 -87
- package/runtime/connectors/base/execution-policy.d.ts +47 -47
- package/runtime/connectors/base/execution-policy.js +82 -82
- package/runtime/connectors/base/index.d.ts +8 -8
- package/runtime/connectors/base/index.js +8 -8
- package/runtime/connectors/base/manifest.d.ts +64 -64
- package/runtime/connectors/base/manifest.js +86 -86
- package/runtime/connectors/base/map-life-evidence.d.ts +16 -16
- package/runtime/connectors/base/map-life-evidence.js +79 -79
- package/runtime/connectors/base/policy-layer.d.ts +29 -29
- package/runtime/connectors/base/policy-layer.js +198 -198
- package/runtime/connectors/base/route-planner.js +99 -99
- package/runtime/connectors/index.d.ts +5 -5
- package/runtime/connectors/index.js +5 -5
- package/runtime/connectors/near-real/near-real-connector-smoke.d.ts +19 -19
- package/runtime/connectors/near-real/near-real-connector-smoke.js +152 -152
- package/runtime/core/second-nature/heartbeat/heartbeat-executor.js +114 -114
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +63 -63
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +139 -139
- package/runtime/core/second-nature/heartbeat/index.d.ts +8 -8
- package/runtime/core/second-nature/heartbeat/index.js +7 -7
- package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.d.ts +21 -21
- package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.js +35 -35
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +28 -28
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +35 -35
- package/runtime/core/second-nature/heartbeat/signal.d.ts +42 -42
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +51 -51
- package/runtime/core/second-nature/index.d.ts +22 -22
- package/runtime/core/second-nature/index.js +22 -22
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +100 -100
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +144 -144
- package/runtime/core/second-nature/orchestrator/guard-layer.d.ts +8 -8
- package/runtime/core/second-nature/orchestrator/guard-layer.js +110 -110
- package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +13 -13
- package/runtime/core/second-nature/orchestrator/intent-planner.js +199 -199
- package/runtime/core/second-nature/orchestrator/lease-manager.d.ts +14 -14
- package/runtime/core/second-nature/orchestrator/lease-manager.js +58 -58
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +6 -6
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +63 -63
- package/runtime/core/second-nature/outreach/delivery-target.d.ts +26 -26
- package/runtime/core/second-nature/outreach/delivery-target.js +70 -70
- package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +38 -38
- package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +119 -119
- package/runtime/core/second-nature/outreach/judge-input-from-snapshot.d.ts +7 -7
- package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +45 -45
- package/runtime/core/second-nature/outreach/judge-outreach.d.ts +40 -40
- package/runtime/core/second-nature/outreach/judge-outreach.js +121 -121
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +21 -21
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +123 -123
- package/runtime/core/second-nature/rhythm/planner-rhythm-window.d.ts +15 -15
- package/runtime/core/second-nature/rhythm/planner-rhythm-window.js +52 -52
- package/runtime/core/second-nature/rhythm/policy-bridge.d.ts +19 -19
- package/runtime/core/second-nature/rhythm/policy-bridge.js +34 -34
- package/runtime/core/second-nature/runtime/service-entry.js +45 -45
- package/runtime/core/second-nature/types.d.ts +51 -51
- package/runtime/guidance/draft-outreach-message.d.ts +7 -7
- package/runtime/guidance/draft-outreach-message.js +42 -42
- package/runtime/guidance/evidence-guidance.d.ts +40 -40
- package/runtime/guidance/evidence-guidance.js +52 -52
- package/runtime/guidance/index.d.ts +11 -11
- package/runtime/guidance/index.js +11 -11
- package/runtime/guidance/outreach-draft-schema.d.ts +228 -228
- package/runtime/guidance/outreach-draft-schema.js +80 -80
- package/runtime/observability/audit/append-only-audit-store.d.ts +14 -14
- package/runtime/observability/audit/append-only-audit-store.js +21 -21
- package/runtime/observability/audit/audit-envelope.d.ts +51 -51
- package/runtime/observability/audit/audit-envelope.js +130 -130
- package/runtime/observability/audit/verify-audit-hash-chain.d.ts +23 -23
- package/runtime/observability/audit/verify-audit-hash-chain.js +83 -83
- package/runtime/observability/db/index.js +124 -124
- package/runtime/observability/db/schema/host-capability-reports.d.ts +180 -180
- package/runtime/observability/db/schema/host-capability-reports.js +12 -12
- package/runtime/observability/db/schema/index.d.ts +947 -947
- package/runtime/observability/db/schema/index.js +71 -71
- package/runtime/observability/index.d.ts +20 -19
- package/runtime/observability/index.js +19 -18
- package/runtime/observability/query/explain-query.d.ts +48 -48
- package/runtime/observability/query/explain-query.js +114 -114
- package/runtime/observability/query/export-audit-bundle.d.ts +22 -22
- package/runtime/observability/query/export-audit-bundle.js +27 -27
- package/runtime/observability/services/decision-ledger.d.ts +46 -46
- package/runtime/observability/services/decision-ledger.js +161 -161
- package/runtime/observability/services/governance-audit.d.ts +41 -41
- package/runtime/observability/services/governance-audit.js +163 -163
- package/runtime/observability/services/governance-plane-recorder.d.ts +47 -47
- package/runtime/observability/services/governance-plane-recorder.js +55 -55
- package/runtime/observability/services/lived-experience-audit.d.ts +97 -97
- package/runtime/observability/services/lived-experience-audit.js +162 -162
- package/runtime/observability/services/runtime-decision-recorder.d.ts +29 -0
- package/runtime/observability/services/runtime-decision-recorder.js +94 -0
- package/runtime/storage/bootstrap/native-sqlite-probe.d.ts +7 -7
- package/runtime/storage/bootstrap/native-sqlite-probe.js +28 -28
- package/runtime/storage/bootstrap/repair-gate.d.ts +17 -17
- package/runtime/storage/bootstrap/repair-gate.js +71 -71
- package/runtime/storage/bootstrap/storage-mode-smoke.d.ts +38 -38
- package/runtime/storage/bootstrap/storage-mode-smoke.js +85 -85
- package/runtime/storage/db/index.js +154 -154
- package/runtime/storage/db/schema/delivery-attempts.d.ts +199 -199
- package/runtime/storage/db/schema/delivery-attempts.js +13 -13
- package/runtime/storage/db/schema/index.d.ts +9 -9
- package/runtime/storage/db/schema/index.js +9 -9
- package/runtime/storage/db/schema/life-evidence-index.d.ts +161 -161
- package/runtime/storage/db/schema/life-evidence-index.js +11 -11
- package/runtime/storage/db/schema/operator-fallback-artifacts.d.ts +161 -161
- package/runtime/storage/db/schema/operator-fallback-artifacts.js +11 -11
- package/runtime/storage/db/schema/policies.d.ts +98 -98
- package/runtime/storage/db/schema/policies.js +8 -8
- package/runtime/storage/delivery/query-delivery-attempts.d.ts +3 -3
- package/runtime/storage/delivery/query-delivery-attempts.js +32 -32
- package/runtime/storage/delivery/types.d.ts +27 -27
- package/runtime/storage/delivery/types.js +1 -1
- package/runtime/storage/delivery/write-delivery-attempt.d.ts +6 -6
- package/runtime/storage/delivery/write-delivery-attempt.js +36 -36
- package/runtime/storage/fallback/load-operator-fallback.d.ts +14 -14
- package/runtime/storage/fallback/load-operator-fallback.js +47 -47
- package/runtime/storage/fallback/operator-fallback-types.d.ts +9 -9
- package/runtime/storage/fallback/operator-fallback-types.js +1 -1
- package/runtime/storage/fallback/operator-fallback-view.d.ts +11 -11
- package/runtime/storage/fallback/operator-fallback-view.js +1 -1
- package/runtime/storage/fallback/write-operator-fallback.d.ts +6 -6
- package/runtime/storage/fallback/write-operator-fallback.js +21 -21
- package/runtime/storage/index.d.ts +37 -37
- package/runtime/storage/index.js +30 -30
- package/runtime/storage/life-evidence/append-life-evidence.d.ts +7 -7
- package/runtime/storage/life-evidence/append-life-evidence.js +64 -64
- package/runtime/storage/life-evidence/types.d.ts +45 -45
- package/runtime/storage/life-evidence/types.js +6 -6
- package/runtime/storage/quiet/persist-quiet-artifact.d.ts +7 -7
- package/runtime/storage/quiet/persist-quiet-artifact.js +22 -22
- package/runtime/storage/quiet/quiet-artifact-types.d.ts +18 -18
- package/runtime/storage/quiet/quiet-artifact-types.js +1 -1
- package/runtime/storage/quiet/quiet-artifact-writer.d.ts +15 -15
- package/runtime/storage/quiet/quiet-artifact-writer.js +56 -56
- package/runtime/storage/repositories/credential-repository.js +30 -30
- package/runtime/storage/rhythm/rhythm-policy-snapshot.d.ts +10 -10
- package/runtime/storage/rhythm/rhythm-policy-snapshot.js +34 -34
- package/runtime/storage/services/credential-vault.d.ts +13 -13
- package/runtime/storage/services/credential-vault.js +116 -116
- package/runtime/storage/snapshots/continuity-snapshot.d.ts +9 -9
- package/runtime/storage/snapshots/continuity-snapshot.js +41 -41
- package/runtime/storage/snapshots/life-evidence-snapshot.d.ts +6 -6
- package/runtime/storage/snapshots/life-evidence-snapshot.js +114 -114
- package/runtime/storage/snapshots/types.d.ts +58 -58
- package/runtime/storage/snapshots/types.js +1 -1
- package/runtime/storage/state-api.js +104 -104
- package/runtime/storage/user-interest/load-user-interest-snapshot.d.ts +2 -2
- package/runtime/storage/user-interest/load-user-interest-snapshot.js +150 -150
- package/runtime/storage/user-interest/types.d.ts +25 -25
- package/runtime/storage/user-interest/types.js +1 -1
- package/workspace-ops-bridge.js +81 -80
package/index.js
CHANGED
|
@@ -1,855 +1,855 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host-safe Second Nature plugin surface.
|
|
3
|
-
*
|
|
4
|
-
* Core logic:
|
|
5
|
-
* - keep register(api) synchronous so OpenClaw captures services/command/tool before return
|
|
6
|
-
* - avoid importing CLI/runtime DB modules at module-evaluation time because the packaged
|
|
7
|
-
* runtime graph currently contains async sql.js bootstrap that breaks vm sandbox loading
|
|
8
|
-
* - expose a minimal in-memory activation spine so status/lifecycle stay truthful even when
|
|
9
|
-
* the full workspace runtime is not loaded inside the host
|
|
10
|
-
*
|
|
11
|
-
* Dependencies:
|
|
12
|
-
* - only imports runtime lifecycle/service modules that are synchronous at load time
|
|
13
|
-
*
|
|
14
|
-
* Boundaries:
|
|
15
|
-
* - read-only operator flows stay available through command/tool surface
|
|
16
|
-
* - structured mutating flows such as policy set / credential verify remain unavailable here
|
|
17
|
-
* - full evidence-backed workspace runtime can be reintroduced later behind a host-safe boundary
|
|
18
|
-
*
|
|
19
|
-
* Plugin classification (verified against OpenClaw 2026.5.4 internals, see
|
|
20
|
-
* docs/validation/openclaw-plugin-classification.md and the explore reports
|
|
21
|
-
* dated 2026-05-06):
|
|
22
|
-
* - Second Nature is a TOOL plugin (exposes `second_nature_ops` to agent
|
|
23
|
-
* sessions). It is intentionally NOT a channel/provider/context-engine.
|
|
24
|
-
* - OpenClaw's `loadGatewayStartupPluginPlan` only loads plugins that opt in
|
|
25
|
-
* via `manifest.activation.onStartup === true`, occupy a configured slot
|
|
26
|
-
* (channel/contextEngine/provider), or declare an explicit hook intent. A
|
|
27
|
-
* tool-only plugin without `activation.onStartup` will be enabled in the
|
|
28
|
-
* registry yet never loaded by the gateway daemon — register(api) only fires
|
|
29
|
-
* inside the `openclaw plugins enable` CLI process, which produces the
|
|
30
|
-
* illusion of a working plugin while agent sessions see no tool. We hit
|
|
31
|
-
* exactly that on 2026-05-06; the fix lives in plugin/openclaw.plugin.json
|
|
32
|
-
* under the `activation` block.
|
|
33
|
-
* - `Shape: non-capability` reported by `openclaw plugins info` is EXPECTED
|
|
34
|
-
* for this plugin. OpenClaw counts capabilities only across cli-backend /
|
|
35
|
-
* text-inference / speech / realtime-* / media-understanding /
|
|
36
|
-
* image-generation / web-search / agent-harness / context-engine / channel.
|
|
37
|
-
* Tool/command/service contributions never bump that count. Pretending to
|
|
38
|
-
* be a context engine with a stub factory just to flip the label would lie
|
|
39
|
-
* to the host (ContextEngine.ingest/assemble/compact get called for real).
|
|
40
|
-
* When Second Nature ships a genuine context-engine layer in a future
|
|
41
|
-
* release, the shape will move to plain-capability honestly.
|
|
42
|
-
*
|
|
43
|
-
* OpenClaw operator norm (T1.1.4 / T1.1.5): set `SECOND_NATURE_WORKSPACE_ROOT` or tool `workspaceRoot` to the
|
|
44
|
-
* **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
|
|
45
|
-
* `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
|
|
46
|
-
* install directory. With **sandbox** or **per-agent workspaces**, use the path where `data/state.db` and
|
|
47
|
-
* `workspace/` anchors actually live. See `explore/reports/2026-05-04_openclaw-plugin-install-vs-workspace-root.md`.
|
|
48
|
-
*
|
|
49
|
-
* Test coverage:
|
|
50
|
-
* - tests/integration/cli/plugin-runtime-registration.test.ts
|
|
51
|
-
* - tests/integration/cli/plugin-packaging-walkthrough.test.ts
|
|
52
|
-
* - tests/integration/cli/plugin-workspace-ops-bridge.test.ts (T1.1.4 / CH-13 matrix, T1.1.5 ops docs cross-ref)
|
|
53
|
-
*/
|
|
54
|
-
import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
|
|
55
|
-
import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
|
|
56
|
-
import { openWorkspaceOpsBridge } from "./workspace-ops-bridge.js";
|
|
57
|
-
// definePluginEntry is OpenClaw's canonical factory for non-channel plugins
|
|
58
|
-
// (provider/tool/command/service/memory/context-engine). At runtime it returns
|
|
59
|
-
// a plain options object; it does NOT add a brand symbol — earlier debugging
|
|
60
|
-
// rounds wrongly assumed the factory was the "plain-capability" marker. The
|
|
61
|
-
// real classification happens via manifest fields (see file header). We still
|
|
62
|
-
// use the factory because it is the documented, supported entry shape, and
|
|
63
|
-
// keeping it future-proof against SDK option-processing changes.
|
|
64
|
-
//
|
|
65
|
-
// IMPORTANT — keep this a STATIC import. The packaged runtime is loaded inside
|
|
66
|
-
// OpenClaw's vm sandbox, which rejects top-level await (manifests as
|
|
67
|
-
// "SyntaxError: Unexpected identifier 'Promise'" at host load time). The same
|
|
68
|
-
// constraint applies to the sql.js async bootstrap noted in the file header.
|
|
69
|
-
// In production the host always provides `openclaw` as a sibling module under
|
|
70
|
-
// ~/.openclaw/npm/node_modules/, so this resolves synchronously. Locally,
|
|
71
|
-
// `openclaw` is declared as a devDependency so build and tests resolve via
|
|
72
|
-
// the same import path.
|
|
73
|
-
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
74
|
-
// Stderr sentinels make daemon load-path observable in `gateway.log`. Three
|
|
75
|
-
// lines should appear at startup: "module evaluated", "register() entered ...",
|
|
76
|
-
// "register() completed". Their absence after `openclaw gateway run` proves
|
|
77
|
-
// the daemon never reached this entry — typically a manifest activation gap.
|
|
78
|
-
process.stderr.write("[second-nature] module evaluated\n");
|
|
79
|
-
const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
|
|
80
|
-
const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
|
|
81
|
-
let activationSpine = null;
|
|
82
|
-
/** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
|
|
83
|
-
let workspaceOpsBridge = null;
|
|
84
|
-
function disposeWorkspaceOpsBridge() {
|
|
85
|
-
if (workspaceOpsBridge) {
|
|
86
|
-
workspaceOpsBridge.close();
|
|
87
|
-
workspaceOpsBridge = null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
const WORKSPACE_BRIDGE_COMMANDS = new Set([
|
|
91
|
-
"status",
|
|
92
|
-
"quiet",
|
|
93
|
-
"report",
|
|
94
|
-
"session",
|
|
95
|
-
"explain",
|
|
96
|
-
"heartbeat_check",
|
|
97
|
-
"fallback",
|
|
98
|
-
"storage_smoke",
|
|
99
|
-
]);
|
|
100
|
-
function isWorkspaceBridgeCommand(command, input) {
|
|
101
|
-
if (command === "credential") {
|
|
102
|
-
const action = typeof input?.action === "string" ? input.action : "show";
|
|
103
|
-
return action !== "verify";
|
|
104
|
-
}
|
|
105
|
-
return WORKSPACE_BRIDGE_COMMANDS.has(command);
|
|
106
|
-
}
|
|
107
|
-
async function ensureWorkspaceOpsBridge(spine) {
|
|
108
|
-
const root = spine.workspaceRootContext.runtimeRoot;
|
|
109
|
-
if (workspaceOpsBridge?.root === root) {
|
|
110
|
-
return { ok: true, dispatch: workspaceOpsBridge.dispatch };
|
|
111
|
-
}
|
|
112
|
-
disposeWorkspaceOpsBridge();
|
|
113
|
-
const opened = await openWorkspaceOpsBridge(root);
|
|
114
|
-
if (!opened.ok) {
|
|
115
|
-
return opened;
|
|
116
|
-
}
|
|
117
|
-
workspaceOpsBridge = { root, close: opened.close, dispatch: opened.dispatch };
|
|
118
|
-
return { ok: true, dispatch: opened.dispatch };
|
|
119
|
-
}
|
|
120
|
-
async function routeSecondNatureCommand(spine, command, input) {
|
|
121
|
-
const wr = spine.workspaceRootContext;
|
|
122
|
-
const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
|
|
123
|
-
if (useBridge) {
|
|
124
|
-
const bridge = await ensureWorkspaceOpsBridge(spine);
|
|
125
|
-
if (!bridge.ok) {
|
|
126
|
-
return {
|
|
127
|
-
ok: false,
|
|
128
|
-
surfaceMode: "host_safe_carrier",
|
|
129
|
-
workspaceReadModelsEvaluated: false,
|
|
130
|
-
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
131
|
-
error: bridge.error,
|
|
132
|
-
data: {
|
|
133
|
-
workspaceRootResolution: wr.resolution,
|
|
134
|
-
bridgeAttempted: true,
|
|
135
|
-
declaredRoot: wr.declaredRoot,
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
return (await bridge.dispatch(command, input));
|
|
140
|
-
}
|
|
141
|
-
const def = spine.router.resolve(command);
|
|
142
|
-
if (!def) {
|
|
143
|
-
return { ok: false, message: `Unknown Second Nature command: ${command}` };
|
|
144
|
-
}
|
|
145
|
-
return def.execute(input);
|
|
146
|
-
}
|
|
147
|
-
function resolveWorkspaceRoot(toolWorkspaceRoot) {
|
|
148
|
-
const env = process.env.SECOND_NATURE_WORKSPACE_ROOT?.trim();
|
|
149
|
-
if (env) {
|
|
150
|
-
return { resolution: "env", declaredRoot: env, runtimeRoot: env };
|
|
151
|
-
}
|
|
152
|
-
const tool = toolWorkspaceRoot?.trim();
|
|
153
|
-
if (tool) {
|
|
154
|
-
return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
|
|
155
|
-
}
|
|
156
|
-
return { resolution: "unknown", declaredRoot: undefined, runtimeRoot: process.cwd() };
|
|
157
|
-
}
|
|
158
|
-
function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
|
|
159
|
-
const next = resolveWorkspaceRoot(toolWorkspaceRoot);
|
|
160
|
-
const prev = spine.workspaceRootContext;
|
|
161
|
-
const changed = next.runtimeRoot !== prev.runtimeRoot || next.resolution !== prev.resolution;
|
|
162
|
-
if (changed) {
|
|
163
|
-
disposeWorkspaceOpsBridge();
|
|
164
|
-
}
|
|
165
|
-
spine.workspaceRootContext = next;
|
|
166
|
-
if (changed) {
|
|
167
|
-
spine.runtimeHandle = startRuntimeService({ workspaceRoot: next.runtimeRoot });
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
function trimRuntimeEvidence(spine) {
|
|
171
|
-
if (spine.runtimeEvidence.length > 12) {
|
|
172
|
-
spine.runtimeEvidence.splice(0, spine.runtimeEvidence.length - 12);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function latestRuntimeEvidence(spine) {
|
|
176
|
-
return spine.runtimeEvidence[spine.runtimeEvidence.length - 1];
|
|
177
|
-
}
|
|
178
|
-
function createUnavailableActionError(code, message, requiredUserInput, nextStep) {
|
|
179
|
-
return {
|
|
180
|
-
ok: false,
|
|
181
|
-
error: {
|
|
182
|
-
code,
|
|
183
|
-
message,
|
|
184
|
-
requiredUserInput,
|
|
185
|
-
nextStep,
|
|
186
|
-
},
|
|
187
|
-
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
function parseExplainSubject(subjectRaw) {
|
|
191
|
-
const trimmed = subjectRaw.trim();
|
|
192
|
-
if (!trimmed) {
|
|
193
|
-
throw new Error("explain_subject_invalid");
|
|
194
|
-
}
|
|
195
|
-
const separatorIndex = trimmed.indexOf(":");
|
|
196
|
-
if (separatorIndex === -1) {
|
|
197
|
-
throw new Error("explain_subject_requires_id");
|
|
198
|
-
}
|
|
199
|
-
const kind = trimmed.slice(0, separatorIndex).trim();
|
|
200
|
-
const id = trimmed.slice(separatorIndex + 1).trim();
|
|
201
|
-
if (!id) {
|
|
202
|
-
throw new Error("explain_subject_requires_id");
|
|
203
|
-
}
|
|
204
|
-
switch (kind) {
|
|
205
|
-
case "decision":
|
|
206
|
-
return { subjectType: "decision", subjectId: id };
|
|
207
|
-
case "platform":
|
|
208
|
-
case "platform-selection":
|
|
209
|
-
return { subjectType: "platform-selection", subjectId: id };
|
|
210
|
-
case "outreach":
|
|
211
|
-
return { subjectType: "outreach", subjectId: id };
|
|
212
|
-
case "soul":
|
|
213
|
-
case "soul-change":
|
|
214
|
-
return { subjectType: "soul-change", subjectId: id };
|
|
215
|
-
case "fallback":
|
|
216
|
-
return { subjectType: "fallback", subjectId: id };
|
|
217
|
-
case "probe":
|
|
218
|
-
return { subjectType: "probe", subjectId: id };
|
|
219
|
-
case "report":
|
|
220
|
-
return { subjectType: "report", subjectId: id };
|
|
221
|
-
case "delivery":
|
|
222
|
-
return { subjectType: "delivery", subjectId: id };
|
|
223
|
-
case "source":
|
|
224
|
-
case "source_ref":
|
|
225
|
-
return { subjectType: "source_ref", subjectId: id };
|
|
226
|
-
default:
|
|
227
|
-
throw new Error("explain_subject_unsupported");
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
function buildStatusPayload(spine) {
|
|
231
|
-
const runtimeEvidence = latestRuntimeEvidence(spine);
|
|
232
|
-
const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
|
|
233
|
-
const wr = spine.workspaceRootContext;
|
|
234
|
-
const needsRootHint = wr.resolution === "unknown";
|
|
235
|
-
return {
|
|
236
|
-
ok: false,
|
|
237
|
-
surfaceMode: "host_safe_carrier",
|
|
238
|
-
workspaceReadModelsEvaluated: false,
|
|
239
|
-
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
240
|
-
error: {
|
|
241
|
-
code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
|
|
242
|
-
message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
|
|
243
|
-
requiredUserInput: needsRootHint ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
244
|
-
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
245
|
-
},
|
|
246
|
-
data: {
|
|
247
|
-
workspaceRootResolution: wr.resolution,
|
|
248
|
-
carrier: {
|
|
249
|
-
host: "openclaw-plugin",
|
|
250
|
-
serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
|
|
251
|
-
updatedAt,
|
|
252
|
-
lastRuntimeTraceId: runtimeEvidence?.traceId,
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
function buildQuietPayload(spine, scope) {
|
|
258
|
-
const wr = spine.workspaceRootContext;
|
|
259
|
-
return {
|
|
260
|
-
ok: false,
|
|
261
|
-
surfaceMode: "host_safe_carrier",
|
|
262
|
-
workspaceReadModelsEvaluated: false,
|
|
263
|
-
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
264
|
-
error: {
|
|
265
|
-
code: "QUIET_READ_SURFACE_UNAVAILABLE",
|
|
266
|
-
message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
|
|
267
|
-
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
268
|
-
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
269
|
-
},
|
|
270
|
-
data: {
|
|
271
|
-
scope,
|
|
272
|
-
evaluated: false,
|
|
273
|
-
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
274
|
-
workspaceRootResolution: wr.resolution,
|
|
275
|
-
},
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
function buildReportPayload(spine, day) {
|
|
279
|
-
const wr = spine.workspaceRootContext;
|
|
280
|
-
return {
|
|
281
|
-
ok: false,
|
|
282
|
-
surfaceMode: "host_safe_carrier",
|
|
283
|
-
workspaceReadModelsEvaluated: false,
|
|
284
|
-
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
285
|
-
error: {
|
|
286
|
-
code: "REPORT_READ_SURFACE_UNAVAILABLE",
|
|
287
|
-
message: "Daily report artifacts require workspace runtime.",
|
|
288
|
-
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
289
|
-
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
290
|
-
},
|
|
291
|
-
data: {
|
|
292
|
-
evaluated: false,
|
|
293
|
-
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
294
|
-
day: day && day.trim() ? day : new Date().toISOString().slice(0, 10),
|
|
295
|
-
workspaceRootResolution: wr.resolution,
|
|
296
|
-
},
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
function buildSessionPayload(spine, sessionId) {
|
|
300
|
-
if (!sessionId) {
|
|
301
|
-
return {
|
|
302
|
-
ok: false,
|
|
303
|
-
error: {
|
|
304
|
-
code: "MISSING_SESSION_ID",
|
|
305
|
-
message: "session show requires sessionId",
|
|
306
|
-
requiredUserInput: ["session_id"],
|
|
307
|
-
nextStep: "reinvoke_session_with_session_id",
|
|
308
|
-
},
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
const wr = spine.workspaceRootContext;
|
|
312
|
-
return {
|
|
313
|
-
ok: false,
|
|
314
|
-
surfaceMode: "host_safe_carrier",
|
|
315
|
-
workspaceReadModelsEvaluated: false,
|
|
316
|
-
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
317
|
-
error: {
|
|
318
|
-
code: "SESSION_READ_SURFACE_UNAVAILABLE",
|
|
319
|
-
message: "Session analytics require workspace state database.",
|
|
320
|
-
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
321
|
-
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
322
|
-
},
|
|
323
|
-
data: {
|
|
324
|
-
requestedSessionId: sessionId,
|
|
325
|
-
evaluated: false,
|
|
326
|
-
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
327
|
-
workspaceRootResolution: wr.resolution,
|
|
328
|
-
},
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
function buildCredentialPayload(spine, platformId) {
|
|
332
|
-
const wr = spine.workspaceRootContext;
|
|
333
|
-
return {
|
|
334
|
-
ok: false,
|
|
335
|
-
surfaceMode: "host_safe_carrier",
|
|
336
|
-
workspaceReadModelsEvaluated: false,
|
|
337
|
-
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
338
|
-
error: {
|
|
339
|
-
code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
|
|
340
|
-
message: "Credential inspection requires workspace runtime on this surface.",
|
|
341
|
-
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
342
|
-
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
343
|
-
},
|
|
344
|
-
data: {
|
|
345
|
-
platformId: platformId && platformId.trim() ? platformId : undefined,
|
|
346
|
-
evaluated: false,
|
|
347
|
-
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
348
|
-
workspaceRootResolution: wr.resolution,
|
|
349
|
-
},
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
function buildExplainPayload(spine, subjectRaw) {
|
|
353
|
-
if (!subjectRaw?.trim()) {
|
|
354
|
-
return {
|
|
355
|
-
ok: false,
|
|
356
|
-
error: {
|
|
357
|
-
code: "MISSING_EXPLAIN_SUBJECT",
|
|
358
|
-
message: "explain requires subject",
|
|
359
|
-
requiredUserInput: ["subject"],
|
|
360
|
-
nextStep: "reinvoke_explain_with_subject",
|
|
361
|
-
},
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
let subject;
|
|
365
|
-
try {
|
|
366
|
-
subject = parseExplainSubject(subjectRaw);
|
|
367
|
-
}
|
|
368
|
-
catch (error) {
|
|
369
|
-
const code = error.message;
|
|
370
|
-
if (code === "explain_subject_requires_id") {
|
|
371
|
-
return createUnavailableActionError("EXPLAIN_SUBJECT_REQUIRES_ID", "subject must include identifier", ["subject"], "reinvoke_explain_with_supported_subject");
|
|
372
|
-
}
|
|
373
|
-
if (code === "explain_subject_unsupported") {
|
|
374
|
-
return createUnavailableActionError("EXPLAIN_SUBJECT_UNSUPPORTED", "supported subjects include decision:, platform:, outreach:, soul:, fallback:, delivery:, probe:, report:, source:", ["subject"], "reinvoke_explain_with_supported_subject");
|
|
375
|
-
}
|
|
376
|
-
return createUnavailableActionError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject", ["subject"], "reinvoke_explain_with_supported_subject");
|
|
377
|
-
}
|
|
378
|
-
const wr = spine.workspaceRootContext;
|
|
379
|
-
return {
|
|
380
|
-
ok: false,
|
|
381
|
-
surfaceMode: "host_safe_carrier",
|
|
382
|
-
workspaceReadModelsEvaluated: false,
|
|
383
|
-
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
384
|
-
error: {
|
|
385
|
-
code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
|
|
386
|
-
message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
|
|
387
|
-
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
388
|
-
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
389
|
-
},
|
|
390
|
-
data: {
|
|
391
|
-
subjectType: subject.subjectType,
|
|
392
|
-
evaluated: false,
|
|
393
|
-
workspaceRootResolution: wr.resolution,
|
|
394
|
-
},
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
async function buildStorageSmokePayload(input) {
|
|
398
|
-
try {
|
|
399
|
-
const mod = await import("./runtime/storage/bootstrap/storage-mode-smoke.js");
|
|
400
|
-
const runRepairFixture = Boolean(input?.runRepairFixture);
|
|
401
|
-
const workspaceRoot = typeof input?.workspaceRoot === "string" ? input.workspaceRoot : undefined;
|
|
402
|
-
const data = await mod.runStorageModeSmoke({ runRepairFixture, workspaceRoot });
|
|
403
|
-
return { ok: true, data };
|
|
404
|
-
}
|
|
405
|
-
catch (error) {
|
|
406
|
-
return {
|
|
407
|
-
ok: false,
|
|
408
|
-
message: error instanceof Error ? error.message : String(error),
|
|
409
|
-
error: {
|
|
410
|
-
code: "STORAGE_SMOKE_LOAD_FAILED",
|
|
411
|
-
message: "Could not load packaged storage-mode smoke module",
|
|
412
|
-
nextStep: "rebuild_plugin_runtime_package",
|
|
413
|
-
},
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
function buildFallbackHostSafePayload(ref) {
|
|
418
|
-
if (!ref?.trim()) {
|
|
419
|
-
return {
|
|
420
|
-
ok: false,
|
|
421
|
-
error: {
|
|
422
|
-
code: "MISSING_FALLBACK_REF",
|
|
423
|
-
message: "fallback requires ref (e.g. fallback:…)",
|
|
424
|
-
requiredUserInput: ["ref"],
|
|
425
|
-
nextStep: "reinvoke_with_ref",
|
|
426
|
-
},
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
return createUnavailableActionError("HOST_SAFE_FALLBACK_VIEW_UNAVAILABLE", "Operator fallback view requires workspace state database; host-safe plugin cannot read persisted fallback artifacts.", ["ref"], "run_workspace_second_nature_cli_or_full_runtime_package");
|
|
430
|
-
}
|
|
431
|
-
function isProbeOnlyInput(input) {
|
|
432
|
-
const v = input?.probeOnly;
|
|
433
|
-
return v === true || v === "true" || v === 1 || v === "1";
|
|
434
|
-
}
|
|
435
|
-
function buildHeartbeatCheckPayload(spine, input) {
|
|
436
|
-
const runtimeEvidence = latestRuntimeEvidence(spine);
|
|
437
|
-
const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
|
|
438
|
-
const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0 ? input.timestamp : updatedAt;
|
|
439
|
-
const wr = spine.workspaceRootContext;
|
|
440
|
-
if (isProbeOnlyInput(input)) {
|
|
441
|
-
return {
|
|
442
|
-
ok: true,
|
|
443
|
-
status: "heartbeat_ok",
|
|
444
|
-
surfaceMode: "capability_probe",
|
|
445
|
-
reasons: ["probe_only"],
|
|
446
|
-
livedExperienceLoopClaimed: false,
|
|
447
|
-
scope: "rhythm",
|
|
448
|
-
trigger: "heartbeat_bridge",
|
|
449
|
-
message: "Capability probe only on the host-safe carrier surface; does not claim a full lived-experience decision loop.",
|
|
450
|
-
data: {
|
|
451
|
-
workspaceRootResolution: wr.resolution,
|
|
452
|
-
runtime: {
|
|
453
|
-
host: "openclaw-plugin",
|
|
454
|
-
serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
|
|
455
|
-
updatedAt,
|
|
456
|
-
},
|
|
457
|
-
surface: {
|
|
458
|
-
tool: "second_nature_ops",
|
|
459
|
-
command: "second-nature heartbeat_check",
|
|
460
|
-
},
|
|
461
|
-
bridge: {
|
|
462
|
-
timestamp,
|
|
463
|
-
probeOnly: true,
|
|
464
|
-
sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
|
|
465
|
-
heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
|
|
466
|
-
serviceEntryMode: "capability_probe",
|
|
467
|
-
},
|
|
468
|
-
},
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
return {
|
|
472
|
-
ok: true,
|
|
473
|
-
status: "runtime_carrier_only",
|
|
474
|
-
surfaceMode: "host_safe_carrier",
|
|
475
|
-
livedExperienceLoopClaimed: false,
|
|
476
|
-
scope: "rhythm",
|
|
477
|
-
trigger: "heartbeat_bridge",
|
|
478
|
-
reasons: ["runtime_carrier_only", "host_safe_bridge_ack"],
|
|
479
|
-
nextAction: "continue_carrier_surface_only",
|
|
480
|
-
message: "Packaged carrier acknowledged this heartbeat round. This is not a full lived-experience decision loop; use the workspace CLI when read models are required.",
|
|
481
|
-
data: {
|
|
482
|
-
workspaceRootResolution: wr.resolution,
|
|
483
|
-
runtime: {
|
|
484
|
-
host: "openclaw-plugin",
|
|
485
|
-
serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
|
|
486
|
-
updatedAt,
|
|
487
|
-
},
|
|
488
|
-
surface: {
|
|
489
|
-
tool: "second_nature_ops",
|
|
490
|
-
command: "second-nature heartbeat_check",
|
|
491
|
-
},
|
|
492
|
-
bridge: {
|
|
493
|
-
timestamp,
|
|
494
|
-
sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
|
|
495
|
-
heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
|
|
496
|
-
serviceEntryMode: "runtime_carrier_only",
|
|
497
|
-
},
|
|
498
|
-
},
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
function createHostSafeRouter(spine) {
|
|
502
|
-
const notImplemented = async (command) => ({
|
|
503
|
-
ok: false,
|
|
504
|
-
command,
|
|
505
|
-
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
506
|
-
});
|
|
507
|
-
const commands = [
|
|
508
|
-
{
|
|
509
|
-
name: "status",
|
|
510
|
-
description: "Show aggregated Second Nature status",
|
|
511
|
-
execute: async () => buildStatusPayload(spine),
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
name: "policy",
|
|
515
|
-
description: "Write or inspect policy state",
|
|
516
|
-
execute: async (input) => {
|
|
517
|
-
const action = typeof input?.action === "string" ? input.action : "show";
|
|
518
|
-
if (action === "set") {
|
|
519
|
-
return createUnavailableActionError("HOST_SAFE_POLICY_SET_UNAVAILABLE", "policy set is unavailable in the host-safe plugin package", ["social_daily_limit", "quiet_enabled"], "run_workspace_runtime_or_reinstall_full_build");
|
|
520
|
-
}
|
|
521
|
-
return notImplemented("policy");
|
|
522
|
-
},
|
|
523
|
-
},
|
|
524
|
-
{
|
|
525
|
-
name: "credential",
|
|
526
|
-
description: "Inspect or recover credential state",
|
|
527
|
-
execute: async (input) => {
|
|
528
|
-
const action = typeof input?.action === "string" ? input.action : "show";
|
|
529
|
-
if (action === "verify") {
|
|
530
|
-
return createUnavailableActionError("HOST_SAFE_CREDENTIAL_VERIFY_UNAVAILABLE", "credential verify is unavailable in the host-safe plugin package", ["verification_answer"], "run_workspace_runtime_or_reinstall_full_build");
|
|
531
|
-
}
|
|
532
|
-
const platformId = typeof input?.platformId === "string" ? input.platformId : undefined;
|
|
533
|
-
return buildCredentialPayload(spine, platformId);
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
name: "quiet",
|
|
538
|
-
description: "Inspect Quiet lifecycle state",
|
|
539
|
-
execute: async (input) => {
|
|
540
|
-
const scope = typeof input?.scope === "string" ? input.scope : undefined;
|
|
541
|
-
return buildQuietPayload(spine, scope);
|
|
542
|
-
},
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
name: "report",
|
|
546
|
-
description: "Show daily report artifacts",
|
|
547
|
-
execute: async (input) => {
|
|
548
|
-
const day = typeof input?.day === "string" ? input.day : undefined;
|
|
549
|
-
return buildReportPayload(spine, day);
|
|
550
|
-
},
|
|
551
|
-
},
|
|
552
|
-
{
|
|
553
|
-
name: "session",
|
|
554
|
-
description: "Inspect continuity session details",
|
|
555
|
-
execute: async (input) => {
|
|
556
|
-
const sessionId = typeof input?.sessionId === "string" ? input.sessionId : undefined;
|
|
557
|
-
return buildSessionPayload(spine, sessionId);
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
{
|
|
561
|
-
name: "audit",
|
|
562
|
-
description: "Inspect audit and evidence views",
|
|
563
|
-
execute: async () => notImplemented("audit"),
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
name: "explain",
|
|
567
|
-
description: "Answer why-question explain requests",
|
|
568
|
-
execute: async (input) => {
|
|
569
|
-
const subject = typeof input?.subject === "string" ? input.subject : undefined;
|
|
570
|
-
return buildExplainPayload(spine, subject);
|
|
571
|
-
},
|
|
572
|
-
},
|
|
573
|
-
{
|
|
574
|
-
name: "heartbeat_check",
|
|
575
|
-
description: "Acknowledge the shipping heartbeat bridge round",
|
|
576
|
-
execute: async (input) => buildHeartbeatCheckPayload(spine, input),
|
|
577
|
-
},
|
|
578
|
-
{
|
|
579
|
-
name: "fallback",
|
|
580
|
-
description: "Operator-visible delivery fallback view (full workspace runtime required)",
|
|
581
|
-
execute: async (input) => {
|
|
582
|
-
const ref = typeof input?.ref === "string" ? input.ref.trim() : undefined;
|
|
583
|
-
return buildFallbackHostSafePayload(ref);
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
{
|
|
587
|
-
name: "storage_smoke",
|
|
588
|
-
description: "T4.1.4 storage mode smoke report (sql.js vs native probe)",
|
|
589
|
-
execute: async (input) => buildStorageSmokePayload(input),
|
|
590
|
-
},
|
|
591
|
-
];
|
|
592
|
-
return {
|
|
593
|
-
commands,
|
|
594
|
-
resolve(name) {
|
|
595
|
-
return commands.find((command) => command.name === name);
|
|
596
|
-
},
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
function createActivationSpine() {
|
|
600
|
-
const workspaceRootContext = resolveWorkspaceRoot(undefined);
|
|
601
|
-
const spine = {
|
|
602
|
-
router: undefined,
|
|
603
|
-
runtimeHandle: startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot }),
|
|
604
|
-
lifecycleState: getLifecycleState(),
|
|
605
|
-
serviceStartRecorded: false,
|
|
606
|
-
runtimeEvidence: [],
|
|
607
|
-
workspaceRootContext,
|
|
608
|
-
};
|
|
609
|
-
spine.router = createHostSafeRouter(spine);
|
|
610
|
-
return spine;
|
|
611
|
-
}
|
|
612
|
-
function ensureActivationSpine() {
|
|
613
|
-
if (activationSpine) {
|
|
614
|
-
return activationSpine;
|
|
615
|
-
}
|
|
616
|
-
activationSpine = createActivationSpine();
|
|
617
|
-
return activationSpine;
|
|
618
|
-
}
|
|
619
|
-
function recordRuntimeEvidence(spine, origin) {
|
|
620
|
-
if (origin === "service_start" && spine.serviceStartRecorded) {
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
if (origin === "service_start") {
|
|
624
|
-
spine.serviceStartRecorded = true;
|
|
625
|
-
}
|
|
626
|
-
spine.runtimeEvidence.push({
|
|
627
|
-
traceId: `${INTERNAL_RUNTIME_TRACE_PREFIX}${origin}-${spine.lifecycleState.registerCount}-${Date.now()}`,
|
|
628
|
-
capability: origin === "register"
|
|
629
|
-
? spine.lifecycleState.registerCount === 1
|
|
630
|
-
? "runtime.activate"
|
|
631
|
-
: "runtime.reload"
|
|
632
|
-
: "runtime.heartbeat",
|
|
633
|
-
origin,
|
|
634
|
-
createdAt: new Date().toISOString(),
|
|
635
|
-
status: "succeeded",
|
|
636
|
-
});
|
|
637
|
-
trimRuntimeEvidence(spine);
|
|
638
|
-
}
|
|
639
|
-
function refreshRegistrationState() {
|
|
640
|
-
const spine = ensureActivationSpine();
|
|
641
|
-
const workspaceRootContext = resolveWorkspaceRoot(undefined);
|
|
642
|
-
const prev = spine.workspaceRootContext;
|
|
643
|
-
const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot || workspaceRootContext.resolution !== prev.resolution;
|
|
644
|
-
if (changed) {
|
|
645
|
-
disposeWorkspaceOpsBridge();
|
|
646
|
-
}
|
|
647
|
-
spine.workspaceRootContext = workspaceRootContext;
|
|
648
|
-
spine.runtimeHandle = startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot });
|
|
649
|
-
spine.lifecycleState = recordRegistration();
|
|
650
|
-
spine.serviceStartRecorded = false;
|
|
651
|
-
recordRuntimeEvidence(spine, "register");
|
|
652
|
-
return spine;
|
|
653
|
-
}
|
|
654
|
-
function parseCommandInput(rawArgs) {
|
|
655
|
-
const tokens = rawArgs?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
656
|
-
if (tokens.length === 0) {
|
|
657
|
-
return {
|
|
658
|
-
ok: false,
|
|
659
|
-
result: { ok: false, message: "Missing command argument." },
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
const [command, ...rest] = tokens;
|
|
663
|
-
if (command === "policy" && rest[0] === "set") {
|
|
664
|
-
return {
|
|
665
|
-
ok: false,
|
|
666
|
-
result: {
|
|
667
|
-
ok: false,
|
|
668
|
-
command,
|
|
669
|
-
message: "policy set requires structured args; use second_nature_ops instead.",
|
|
670
|
-
},
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
if (command === "credential" && rest[0] === "verify") {
|
|
674
|
-
return {
|
|
675
|
-
ok: false,
|
|
676
|
-
result: {
|
|
677
|
-
ok: false,
|
|
678
|
-
command,
|
|
679
|
-
message: "credential verify requires structured args; use second_nature_ops instead.",
|
|
680
|
-
},
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
switch (command) {
|
|
684
|
-
case "status":
|
|
685
|
-
case "quiet":
|
|
686
|
-
return {
|
|
687
|
-
ok: true,
|
|
688
|
-
command,
|
|
689
|
-
input: rest.length > 0 ? { scope: rest.join(" ") } : undefined,
|
|
690
|
-
};
|
|
691
|
-
case "report":
|
|
692
|
-
return {
|
|
693
|
-
ok: true,
|
|
694
|
-
command,
|
|
695
|
-
input: rest[0] ? { day: rest[0] } : undefined,
|
|
696
|
-
};
|
|
697
|
-
case "session":
|
|
698
|
-
return {
|
|
699
|
-
ok: true,
|
|
700
|
-
command,
|
|
701
|
-
input: rest[0] ? { sessionId: rest[0] } : undefined,
|
|
702
|
-
};
|
|
703
|
-
case "credential":
|
|
704
|
-
return {
|
|
705
|
-
ok: true,
|
|
706
|
-
command,
|
|
707
|
-
input: rest[0] ? { platformId: rest[0] } : undefined,
|
|
708
|
-
};
|
|
709
|
-
case "heartbeat_check":
|
|
710
|
-
return {
|
|
711
|
-
ok: true,
|
|
712
|
-
command,
|
|
713
|
-
input: rest.length > 0
|
|
714
|
-
? {
|
|
715
|
-
timestamp: rest[0],
|
|
716
|
-
sessionContext: rest.length > 1 ? rest.slice(1).join(" ") : undefined,
|
|
717
|
-
}
|
|
718
|
-
: undefined,
|
|
719
|
-
};
|
|
720
|
-
case "explain":
|
|
721
|
-
return {
|
|
722
|
-
ok: true,
|
|
723
|
-
command,
|
|
724
|
-
input: rest.length > 0 ? { subject: rest.join(" ") } : undefined,
|
|
725
|
-
};
|
|
726
|
-
case "fallback":
|
|
727
|
-
return {
|
|
728
|
-
ok: true,
|
|
729
|
-
command,
|
|
730
|
-
input: rest.length > 0 ? { ref: rest.join(" ") } : undefined,
|
|
731
|
-
};
|
|
732
|
-
case "storage_smoke": {
|
|
733
|
-
const wantRepair = rest[0] === "repair" || rest.includes("--repair");
|
|
734
|
-
return {
|
|
735
|
-
ok: true,
|
|
736
|
-
command,
|
|
737
|
-
input: wantRepair ? { runRepairFixture: true } : undefined,
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
default:
|
|
741
|
-
return {
|
|
742
|
-
ok: true,
|
|
743
|
-
command,
|
|
744
|
-
input: undefined,
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
function createRuntimeService() {
|
|
749
|
-
return {
|
|
750
|
-
id: "second-nature-runtime",
|
|
751
|
-
start() {
|
|
752
|
-
const spine = ensureActivationSpine();
|
|
753
|
-
recordRuntimeEvidence(spine, "service_start");
|
|
754
|
-
return {
|
|
755
|
-
ready: spine.runtimeHandle.ready,
|
|
756
|
-
version: spine.runtimeHandle.version,
|
|
757
|
-
};
|
|
758
|
-
},
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
function createLifecycleService() {
|
|
762
|
-
return {
|
|
763
|
-
id: "second-nature-lifecycle",
|
|
764
|
-
start() {
|
|
765
|
-
const spine = ensureActivationSpine();
|
|
766
|
-
return {
|
|
767
|
-
phase: spine.lifecycleState.phase,
|
|
768
|
-
registerCount: spine.lifecycleState.registerCount,
|
|
769
|
-
lastChangedAt: spine.lifecycleState.lastChangedAt,
|
|
770
|
-
};
|
|
771
|
-
},
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
const SECOND_NATURE_TOOL_SCHEMA = {
|
|
775
|
-
type: "object",
|
|
776
|
-
additionalProperties: false,
|
|
777
|
-
properties: {
|
|
778
|
-
command: { type: "string" },
|
|
779
|
-
args: { type: "object", additionalProperties: true },
|
|
780
|
-
workspaceRoot: {
|
|
781
|
-
type: "string",
|
|
782
|
-
description: "Workspace root for packaged smoke/runtime alignment (optional; prefer SECOND_NATURE_WORKSPACE_ROOT).",
|
|
783
|
-
},
|
|
784
|
-
},
|
|
785
|
-
required: ["command"],
|
|
786
|
-
};
|
|
787
|
-
export default definePluginEntry({
|
|
788
|
-
id: "second-nature",
|
|
789
|
-
name: "Second Nature",
|
|
790
|
-
description: "Registers command/tool/service surface with load-reload lifecycle semantics.",
|
|
791
|
-
register(api) {
|
|
792
|
-
process.stderr.write(`[second-nature] register() entered, api keys=${Object.keys(api).join(",")}\n`);
|
|
793
|
-
const runtimeService = createRuntimeService();
|
|
794
|
-
const lifecycleService = createLifecycleService();
|
|
795
|
-
api.registerService(runtimeService);
|
|
796
|
-
api.registerService(lifecycleService);
|
|
797
|
-
api.registerCommand({
|
|
798
|
-
name: "second-nature",
|
|
799
|
-
description: "Route Agent-facing operational commands for Second Nature.",
|
|
800
|
-
acceptsArgs: true,
|
|
801
|
-
handler: async (ctx) => {
|
|
802
|
-
const spine = ensureActivationSpine();
|
|
803
|
-
const parsed = parseCommandInput(ctx.args);
|
|
804
|
-
if (!parsed.ok) {
|
|
805
|
-
return {
|
|
806
|
-
text: JSON.stringify(parsed.result),
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
const resolved = spine.router.resolve(parsed.command);
|
|
810
|
-
if (!resolved) {
|
|
811
|
-
return {
|
|
812
|
-
text: JSON.stringify({ ok: false, command: parsed.command, message: "Unknown Second Nature command." }),
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
|
|
816
|
-
return {
|
|
817
|
-
text: JSON.stringify(result),
|
|
818
|
-
};
|
|
819
|
-
},
|
|
820
|
-
});
|
|
821
|
-
const executeSecondNatureTool = async (params) => {
|
|
822
|
-
const spine = ensureActivationSpine();
|
|
823
|
-
syncWorkspaceRootFromTool(spine, params.workspaceRoot);
|
|
824
|
-
const resolved = spine.router.resolve(params.command);
|
|
825
|
-
if (!resolved) {
|
|
826
|
-
return {
|
|
827
|
-
content: [
|
|
828
|
-
{
|
|
829
|
-
type: "text",
|
|
830
|
-
text: JSON.stringify({ ok: false, message: "Unknown Second Nature command." }),
|
|
831
|
-
},
|
|
832
|
-
],
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
const result = await routeSecondNatureCommand(spine, params.command, params.args);
|
|
836
|
-
return {
|
|
837
|
-
content: [
|
|
838
|
-
{
|
|
839
|
-
type: "text",
|
|
840
|
-
text: JSON.stringify(result),
|
|
841
|
-
},
|
|
842
|
-
],
|
|
843
|
-
};
|
|
844
|
-
};
|
|
845
|
-
api.registerTool({
|
|
846
|
-
name: "second_nature_ops",
|
|
847
|
-
description: "Access the Second Nature command surface through a single tool shell.",
|
|
848
|
-
parameters: SECOND_NATURE_TOOL_SCHEMA,
|
|
849
|
-
async execute(_id, params) {
|
|
850
|
-
return executeSecondNatureTool(params);
|
|
851
|
-
},
|
|
852
|
-
});
|
|
853
|
-
process.stderr.write("[second-nature] register() completed\n");
|
|
854
|
-
},
|
|
855
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Host-safe Second Nature plugin surface.
|
|
3
|
+
*
|
|
4
|
+
* Core logic:
|
|
5
|
+
* - keep register(api) synchronous so OpenClaw captures services/command/tool before return
|
|
6
|
+
* - avoid importing CLI/runtime DB modules at module-evaluation time because the packaged
|
|
7
|
+
* runtime graph currently contains async sql.js bootstrap that breaks vm sandbox loading
|
|
8
|
+
* - expose a minimal in-memory activation spine so status/lifecycle stay truthful even when
|
|
9
|
+
* the full workspace runtime is not loaded inside the host
|
|
10
|
+
*
|
|
11
|
+
* Dependencies:
|
|
12
|
+
* - only imports runtime lifecycle/service modules that are synchronous at load time
|
|
13
|
+
*
|
|
14
|
+
* Boundaries:
|
|
15
|
+
* - read-only operator flows stay available through command/tool surface
|
|
16
|
+
* - structured mutating flows such as policy set / credential verify remain unavailable here
|
|
17
|
+
* - full evidence-backed workspace runtime can be reintroduced later behind a host-safe boundary
|
|
18
|
+
*
|
|
19
|
+
* Plugin classification (verified against OpenClaw 2026.5.4 internals, see
|
|
20
|
+
* docs/validation/openclaw-plugin-classification.md and the explore reports
|
|
21
|
+
* dated 2026-05-06):
|
|
22
|
+
* - Second Nature is a TOOL plugin (exposes `second_nature_ops` to agent
|
|
23
|
+
* sessions). It is intentionally NOT a channel/provider/context-engine.
|
|
24
|
+
* - OpenClaw's `loadGatewayStartupPluginPlan` only loads plugins that opt in
|
|
25
|
+
* via `manifest.activation.onStartup === true`, occupy a configured slot
|
|
26
|
+
* (channel/contextEngine/provider), or declare an explicit hook intent. A
|
|
27
|
+
* tool-only plugin without `activation.onStartup` will be enabled in the
|
|
28
|
+
* registry yet never loaded by the gateway daemon — register(api) only fires
|
|
29
|
+
* inside the `openclaw plugins enable` CLI process, which produces the
|
|
30
|
+
* illusion of a working plugin while agent sessions see no tool. We hit
|
|
31
|
+
* exactly that on 2026-05-06; the fix lives in plugin/openclaw.plugin.json
|
|
32
|
+
* under the `activation` block.
|
|
33
|
+
* - `Shape: non-capability` reported by `openclaw plugins info` is EXPECTED
|
|
34
|
+
* for this plugin. OpenClaw counts capabilities only across cli-backend /
|
|
35
|
+
* text-inference / speech / realtime-* / media-understanding /
|
|
36
|
+
* image-generation / web-search / agent-harness / context-engine / channel.
|
|
37
|
+
* Tool/command/service contributions never bump that count. Pretending to
|
|
38
|
+
* be a context engine with a stub factory just to flip the label would lie
|
|
39
|
+
* to the host (ContextEngine.ingest/assemble/compact get called for real).
|
|
40
|
+
* When Second Nature ships a genuine context-engine layer in a future
|
|
41
|
+
* release, the shape will move to plain-capability honestly.
|
|
42
|
+
*
|
|
43
|
+
* OpenClaw operator norm (T1.1.4 / T1.1.5): set `SECOND_NATURE_WORKSPACE_ROOT` or tool `workspaceRoot` to the
|
|
44
|
+
* **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
|
|
45
|
+
* `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
|
|
46
|
+
* install directory. With **sandbox** or **per-agent workspaces**, use the path where `data/state.db` and
|
|
47
|
+
* `workspace/` anchors actually live. See `explore/reports/2026-05-04_openclaw-plugin-install-vs-workspace-root.md`.
|
|
48
|
+
*
|
|
49
|
+
* Test coverage:
|
|
50
|
+
* - tests/integration/cli/plugin-runtime-registration.test.ts
|
|
51
|
+
* - tests/integration/cli/plugin-packaging-walkthrough.test.ts
|
|
52
|
+
* - tests/integration/cli/plugin-workspace-ops-bridge.test.ts (T1.1.4 / CH-13 matrix, T1.1.5 ops docs cross-ref)
|
|
53
|
+
*/
|
|
54
|
+
import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
|
|
55
|
+
import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
|
|
56
|
+
import { openWorkspaceOpsBridge } from "./workspace-ops-bridge.js";
|
|
57
|
+
// definePluginEntry is OpenClaw's canonical factory for non-channel plugins
|
|
58
|
+
// (provider/tool/command/service/memory/context-engine). At runtime it returns
|
|
59
|
+
// a plain options object; it does NOT add a brand symbol — earlier debugging
|
|
60
|
+
// rounds wrongly assumed the factory was the "plain-capability" marker. The
|
|
61
|
+
// real classification happens via manifest fields (see file header). We still
|
|
62
|
+
// use the factory because it is the documented, supported entry shape, and
|
|
63
|
+
// keeping it future-proof against SDK option-processing changes.
|
|
64
|
+
//
|
|
65
|
+
// IMPORTANT — keep this a STATIC import. The packaged runtime is loaded inside
|
|
66
|
+
// OpenClaw's vm sandbox, which rejects top-level await (manifests as
|
|
67
|
+
// "SyntaxError: Unexpected identifier 'Promise'" at host load time). The same
|
|
68
|
+
// constraint applies to the sql.js async bootstrap noted in the file header.
|
|
69
|
+
// In production the host always provides `openclaw` as a sibling module under
|
|
70
|
+
// ~/.openclaw/npm/node_modules/, so this resolves synchronously. Locally,
|
|
71
|
+
// `openclaw` is declared as a devDependency so build and tests resolve via
|
|
72
|
+
// the same import path.
|
|
73
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
74
|
+
// Stderr sentinels make daemon load-path observable in `gateway.log`. Three
|
|
75
|
+
// lines should appear at startup: "module evaluated", "register() entered ...",
|
|
76
|
+
// "register() completed". Their absence after `openclaw gateway run` proves
|
|
77
|
+
// the daemon never reached this entry — typically a manifest activation gap.
|
|
78
|
+
process.stderr.write("[second-nature] module evaluated\n");
|
|
79
|
+
const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
|
|
80
|
+
const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
|
|
81
|
+
let activationSpine = null;
|
|
82
|
+
/** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
|
|
83
|
+
let workspaceOpsBridge = null;
|
|
84
|
+
function disposeWorkspaceOpsBridge() {
|
|
85
|
+
if (workspaceOpsBridge) {
|
|
86
|
+
workspaceOpsBridge.close();
|
|
87
|
+
workspaceOpsBridge = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const WORKSPACE_BRIDGE_COMMANDS = new Set([
|
|
91
|
+
"status",
|
|
92
|
+
"quiet",
|
|
93
|
+
"report",
|
|
94
|
+
"session",
|
|
95
|
+
"explain",
|
|
96
|
+
"heartbeat_check",
|
|
97
|
+
"fallback",
|
|
98
|
+
"storage_smoke",
|
|
99
|
+
]);
|
|
100
|
+
function isWorkspaceBridgeCommand(command, input) {
|
|
101
|
+
if (command === "credential") {
|
|
102
|
+
const action = typeof input?.action === "string" ? input.action : "show";
|
|
103
|
+
return action !== "verify";
|
|
104
|
+
}
|
|
105
|
+
return WORKSPACE_BRIDGE_COMMANDS.has(command);
|
|
106
|
+
}
|
|
107
|
+
async function ensureWorkspaceOpsBridge(spine) {
|
|
108
|
+
const root = spine.workspaceRootContext.runtimeRoot;
|
|
109
|
+
if (workspaceOpsBridge?.root === root) {
|
|
110
|
+
return { ok: true, dispatch: workspaceOpsBridge.dispatch };
|
|
111
|
+
}
|
|
112
|
+
disposeWorkspaceOpsBridge();
|
|
113
|
+
const opened = await openWorkspaceOpsBridge(root);
|
|
114
|
+
if (!opened.ok) {
|
|
115
|
+
return opened;
|
|
116
|
+
}
|
|
117
|
+
workspaceOpsBridge = { root, close: opened.close, dispatch: opened.dispatch };
|
|
118
|
+
return { ok: true, dispatch: opened.dispatch };
|
|
119
|
+
}
|
|
120
|
+
async function routeSecondNatureCommand(spine, command, input) {
|
|
121
|
+
const wr = spine.workspaceRootContext;
|
|
122
|
+
const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
|
|
123
|
+
if (useBridge) {
|
|
124
|
+
const bridge = await ensureWorkspaceOpsBridge(spine);
|
|
125
|
+
if (!bridge.ok) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
surfaceMode: "host_safe_carrier",
|
|
129
|
+
workspaceReadModelsEvaluated: false,
|
|
130
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
131
|
+
error: bridge.error,
|
|
132
|
+
data: {
|
|
133
|
+
workspaceRootResolution: wr.resolution,
|
|
134
|
+
bridgeAttempted: true,
|
|
135
|
+
declaredRoot: wr.declaredRoot,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return (await bridge.dispatch(command, input));
|
|
140
|
+
}
|
|
141
|
+
const def = spine.router.resolve(command);
|
|
142
|
+
if (!def) {
|
|
143
|
+
return { ok: false, message: `Unknown Second Nature command: ${command}` };
|
|
144
|
+
}
|
|
145
|
+
return def.execute(input);
|
|
146
|
+
}
|
|
147
|
+
function resolveWorkspaceRoot(toolWorkspaceRoot) {
|
|
148
|
+
const env = process.env.SECOND_NATURE_WORKSPACE_ROOT?.trim();
|
|
149
|
+
if (env) {
|
|
150
|
+
return { resolution: "env", declaredRoot: env, runtimeRoot: env };
|
|
151
|
+
}
|
|
152
|
+
const tool = toolWorkspaceRoot?.trim();
|
|
153
|
+
if (tool) {
|
|
154
|
+
return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
|
|
155
|
+
}
|
|
156
|
+
return { resolution: "unknown", declaredRoot: undefined, runtimeRoot: process.cwd() };
|
|
157
|
+
}
|
|
158
|
+
function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
|
|
159
|
+
const next = resolveWorkspaceRoot(toolWorkspaceRoot);
|
|
160
|
+
const prev = spine.workspaceRootContext;
|
|
161
|
+
const changed = next.runtimeRoot !== prev.runtimeRoot || next.resolution !== prev.resolution;
|
|
162
|
+
if (changed) {
|
|
163
|
+
disposeWorkspaceOpsBridge();
|
|
164
|
+
}
|
|
165
|
+
spine.workspaceRootContext = next;
|
|
166
|
+
if (changed) {
|
|
167
|
+
spine.runtimeHandle = startRuntimeService({ workspaceRoot: next.runtimeRoot });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function trimRuntimeEvidence(spine) {
|
|
171
|
+
if (spine.runtimeEvidence.length > 12) {
|
|
172
|
+
spine.runtimeEvidence.splice(0, spine.runtimeEvidence.length - 12);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function latestRuntimeEvidence(spine) {
|
|
176
|
+
return spine.runtimeEvidence[spine.runtimeEvidence.length - 1];
|
|
177
|
+
}
|
|
178
|
+
function createUnavailableActionError(code, message, requiredUserInput, nextStep) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: {
|
|
182
|
+
code,
|
|
183
|
+
message,
|
|
184
|
+
requiredUserInput,
|
|
185
|
+
nextStep,
|
|
186
|
+
},
|
|
187
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function parseExplainSubject(subjectRaw) {
|
|
191
|
+
const trimmed = subjectRaw.trim();
|
|
192
|
+
if (!trimmed) {
|
|
193
|
+
throw new Error("explain_subject_invalid");
|
|
194
|
+
}
|
|
195
|
+
const separatorIndex = trimmed.indexOf(":");
|
|
196
|
+
if (separatorIndex === -1) {
|
|
197
|
+
throw new Error("explain_subject_requires_id");
|
|
198
|
+
}
|
|
199
|
+
const kind = trimmed.slice(0, separatorIndex).trim();
|
|
200
|
+
const id = trimmed.slice(separatorIndex + 1).trim();
|
|
201
|
+
if (!id) {
|
|
202
|
+
throw new Error("explain_subject_requires_id");
|
|
203
|
+
}
|
|
204
|
+
switch (kind) {
|
|
205
|
+
case "decision":
|
|
206
|
+
return { subjectType: "decision", subjectId: id };
|
|
207
|
+
case "platform":
|
|
208
|
+
case "platform-selection":
|
|
209
|
+
return { subjectType: "platform-selection", subjectId: id };
|
|
210
|
+
case "outreach":
|
|
211
|
+
return { subjectType: "outreach", subjectId: id };
|
|
212
|
+
case "soul":
|
|
213
|
+
case "soul-change":
|
|
214
|
+
return { subjectType: "soul-change", subjectId: id };
|
|
215
|
+
case "fallback":
|
|
216
|
+
return { subjectType: "fallback", subjectId: id };
|
|
217
|
+
case "probe":
|
|
218
|
+
return { subjectType: "probe", subjectId: id };
|
|
219
|
+
case "report":
|
|
220
|
+
return { subjectType: "report", subjectId: id };
|
|
221
|
+
case "delivery":
|
|
222
|
+
return { subjectType: "delivery", subjectId: id };
|
|
223
|
+
case "source":
|
|
224
|
+
case "source_ref":
|
|
225
|
+
return { subjectType: "source_ref", subjectId: id };
|
|
226
|
+
default:
|
|
227
|
+
throw new Error("explain_subject_unsupported");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function buildStatusPayload(spine) {
|
|
231
|
+
const runtimeEvidence = latestRuntimeEvidence(spine);
|
|
232
|
+
const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
|
|
233
|
+
const wr = spine.workspaceRootContext;
|
|
234
|
+
const needsRootHint = wr.resolution === "unknown";
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
surfaceMode: "host_safe_carrier",
|
|
238
|
+
workspaceReadModelsEvaluated: false,
|
|
239
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
240
|
+
error: {
|
|
241
|
+
code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
|
|
242
|
+
message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
|
|
243
|
+
requiredUserInput: needsRootHint ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
244
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
245
|
+
},
|
|
246
|
+
data: {
|
|
247
|
+
workspaceRootResolution: wr.resolution,
|
|
248
|
+
carrier: {
|
|
249
|
+
host: "openclaw-plugin",
|
|
250
|
+
serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
|
|
251
|
+
updatedAt,
|
|
252
|
+
lastRuntimeTraceId: runtimeEvidence?.traceId,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function buildQuietPayload(spine, scope) {
|
|
258
|
+
const wr = spine.workspaceRootContext;
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
surfaceMode: "host_safe_carrier",
|
|
262
|
+
workspaceReadModelsEvaluated: false,
|
|
263
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
264
|
+
error: {
|
|
265
|
+
code: "QUIET_READ_SURFACE_UNAVAILABLE",
|
|
266
|
+
message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
|
|
267
|
+
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
268
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
269
|
+
},
|
|
270
|
+
data: {
|
|
271
|
+
scope,
|
|
272
|
+
evaluated: false,
|
|
273
|
+
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
274
|
+
workspaceRootResolution: wr.resolution,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function buildReportPayload(spine, day) {
|
|
279
|
+
const wr = spine.workspaceRootContext;
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
surfaceMode: "host_safe_carrier",
|
|
283
|
+
workspaceReadModelsEvaluated: false,
|
|
284
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
285
|
+
error: {
|
|
286
|
+
code: "REPORT_READ_SURFACE_UNAVAILABLE",
|
|
287
|
+
message: "Daily report artifacts require workspace runtime.",
|
|
288
|
+
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
289
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
290
|
+
},
|
|
291
|
+
data: {
|
|
292
|
+
evaluated: false,
|
|
293
|
+
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
294
|
+
day: day && day.trim() ? day : new Date().toISOString().slice(0, 10),
|
|
295
|
+
workspaceRootResolution: wr.resolution,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function buildSessionPayload(spine, sessionId) {
|
|
300
|
+
if (!sessionId) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
error: {
|
|
304
|
+
code: "MISSING_SESSION_ID",
|
|
305
|
+
message: "session show requires sessionId",
|
|
306
|
+
requiredUserInput: ["session_id"],
|
|
307
|
+
nextStep: "reinvoke_session_with_session_id",
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const wr = spine.workspaceRootContext;
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
surfaceMode: "host_safe_carrier",
|
|
315
|
+
workspaceReadModelsEvaluated: false,
|
|
316
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
317
|
+
error: {
|
|
318
|
+
code: "SESSION_READ_SURFACE_UNAVAILABLE",
|
|
319
|
+
message: "Session analytics require workspace state database.",
|
|
320
|
+
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
321
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
322
|
+
},
|
|
323
|
+
data: {
|
|
324
|
+
requestedSessionId: sessionId,
|
|
325
|
+
evaluated: false,
|
|
326
|
+
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
327
|
+
workspaceRootResolution: wr.resolution,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function buildCredentialPayload(spine, platformId) {
|
|
332
|
+
const wr = spine.workspaceRootContext;
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
surfaceMode: "host_safe_carrier",
|
|
336
|
+
workspaceReadModelsEvaluated: false,
|
|
337
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
338
|
+
error: {
|
|
339
|
+
code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
|
|
340
|
+
message: "Credential inspection requires workspace runtime on this surface.",
|
|
341
|
+
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
342
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
343
|
+
},
|
|
344
|
+
data: {
|
|
345
|
+
platformId: platformId && platformId.trim() ? platformId : undefined,
|
|
346
|
+
evaluated: false,
|
|
347
|
+
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
348
|
+
workspaceRootResolution: wr.resolution,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function buildExplainPayload(spine, subjectRaw) {
|
|
353
|
+
if (!subjectRaw?.trim()) {
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
error: {
|
|
357
|
+
code: "MISSING_EXPLAIN_SUBJECT",
|
|
358
|
+
message: "explain requires subject",
|
|
359
|
+
requiredUserInput: ["subject"],
|
|
360
|
+
nextStep: "reinvoke_explain_with_subject",
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
let subject;
|
|
365
|
+
try {
|
|
366
|
+
subject = parseExplainSubject(subjectRaw);
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
const code = error.message;
|
|
370
|
+
if (code === "explain_subject_requires_id") {
|
|
371
|
+
return createUnavailableActionError("EXPLAIN_SUBJECT_REQUIRES_ID", "subject must include identifier", ["subject"], "reinvoke_explain_with_supported_subject");
|
|
372
|
+
}
|
|
373
|
+
if (code === "explain_subject_unsupported") {
|
|
374
|
+
return createUnavailableActionError("EXPLAIN_SUBJECT_UNSUPPORTED", "supported subjects include decision:, platform:, outreach:, soul:, fallback:, delivery:, probe:, report:, source:", ["subject"], "reinvoke_explain_with_supported_subject");
|
|
375
|
+
}
|
|
376
|
+
return createUnavailableActionError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject", ["subject"], "reinvoke_explain_with_supported_subject");
|
|
377
|
+
}
|
|
378
|
+
const wr = spine.workspaceRootContext;
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
surfaceMode: "host_safe_carrier",
|
|
382
|
+
workspaceReadModelsEvaluated: false,
|
|
383
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
384
|
+
error: {
|
|
385
|
+
code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
|
|
386
|
+
message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
|
|
387
|
+
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
388
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
389
|
+
},
|
|
390
|
+
data: {
|
|
391
|
+
subjectType: subject.subjectType,
|
|
392
|
+
evaluated: false,
|
|
393
|
+
workspaceRootResolution: wr.resolution,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async function buildStorageSmokePayload(input) {
|
|
398
|
+
try {
|
|
399
|
+
const mod = await import("./runtime/storage/bootstrap/storage-mode-smoke.js");
|
|
400
|
+
const runRepairFixture = Boolean(input?.runRepairFixture);
|
|
401
|
+
const workspaceRoot = typeof input?.workspaceRoot === "string" ? input.workspaceRoot : undefined;
|
|
402
|
+
const data = await mod.runStorageModeSmoke({ runRepairFixture, workspaceRoot });
|
|
403
|
+
return { ok: true, data };
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
message: error instanceof Error ? error.message : String(error),
|
|
409
|
+
error: {
|
|
410
|
+
code: "STORAGE_SMOKE_LOAD_FAILED",
|
|
411
|
+
message: "Could not load packaged storage-mode smoke module",
|
|
412
|
+
nextStep: "rebuild_plugin_runtime_package",
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function buildFallbackHostSafePayload(ref) {
|
|
418
|
+
if (!ref?.trim()) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
error: {
|
|
422
|
+
code: "MISSING_FALLBACK_REF",
|
|
423
|
+
message: "fallback requires ref (e.g. fallback:…)",
|
|
424
|
+
requiredUserInput: ["ref"],
|
|
425
|
+
nextStep: "reinvoke_with_ref",
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return createUnavailableActionError("HOST_SAFE_FALLBACK_VIEW_UNAVAILABLE", "Operator fallback view requires workspace state database; host-safe plugin cannot read persisted fallback artifacts.", ["ref"], "run_workspace_second_nature_cli_or_full_runtime_package");
|
|
430
|
+
}
|
|
431
|
+
function isProbeOnlyInput(input) {
|
|
432
|
+
const v = input?.probeOnly;
|
|
433
|
+
return v === true || v === "true" || v === 1 || v === "1";
|
|
434
|
+
}
|
|
435
|
+
function buildHeartbeatCheckPayload(spine, input) {
|
|
436
|
+
const runtimeEvidence = latestRuntimeEvidence(spine);
|
|
437
|
+
const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
|
|
438
|
+
const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0 ? input.timestamp : updatedAt;
|
|
439
|
+
const wr = spine.workspaceRootContext;
|
|
440
|
+
if (isProbeOnlyInput(input)) {
|
|
441
|
+
return {
|
|
442
|
+
ok: true,
|
|
443
|
+
status: "heartbeat_ok",
|
|
444
|
+
surfaceMode: "capability_probe",
|
|
445
|
+
reasons: ["probe_only"],
|
|
446
|
+
livedExperienceLoopClaimed: false,
|
|
447
|
+
scope: "rhythm",
|
|
448
|
+
trigger: "heartbeat_bridge",
|
|
449
|
+
message: "Capability probe only on the host-safe carrier surface; does not claim a full lived-experience decision loop.",
|
|
450
|
+
data: {
|
|
451
|
+
workspaceRootResolution: wr.resolution,
|
|
452
|
+
runtime: {
|
|
453
|
+
host: "openclaw-plugin",
|
|
454
|
+
serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
|
|
455
|
+
updatedAt,
|
|
456
|
+
},
|
|
457
|
+
surface: {
|
|
458
|
+
tool: "second_nature_ops",
|
|
459
|
+
command: "second-nature heartbeat_check",
|
|
460
|
+
},
|
|
461
|
+
bridge: {
|
|
462
|
+
timestamp,
|
|
463
|
+
probeOnly: true,
|
|
464
|
+
sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
|
|
465
|
+
heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
|
|
466
|
+
serviceEntryMode: "capability_probe",
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
ok: true,
|
|
473
|
+
status: "runtime_carrier_only",
|
|
474
|
+
surfaceMode: "host_safe_carrier",
|
|
475
|
+
livedExperienceLoopClaimed: false,
|
|
476
|
+
scope: "rhythm",
|
|
477
|
+
trigger: "heartbeat_bridge",
|
|
478
|
+
reasons: ["runtime_carrier_only", "host_safe_bridge_ack"],
|
|
479
|
+
nextAction: "continue_carrier_surface_only",
|
|
480
|
+
message: "Packaged carrier acknowledged this heartbeat round. This is not a full lived-experience decision loop; use the workspace CLI when read models are required.",
|
|
481
|
+
data: {
|
|
482
|
+
workspaceRootResolution: wr.resolution,
|
|
483
|
+
runtime: {
|
|
484
|
+
host: "openclaw-plugin",
|
|
485
|
+
serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
|
|
486
|
+
updatedAt,
|
|
487
|
+
},
|
|
488
|
+
surface: {
|
|
489
|
+
tool: "second_nature_ops",
|
|
490
|
+
command: "second-nature heartbeat_check",
|
|
491
|
+
},
|
|
492
|
+
bridge: {
|
|
493
|
+
timestamp,
|
|
494
|
+
sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
|
|
495
|
+
heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
|
|
496
|
+
serviceEntryMode: "runtime_carrier_only",
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function createHostSafeRouter(spine) {
|
|
502
|
+
const notImplemented = async (command) => ({
|
|
503
|
+
ok: false,
|
|
504
|
+
command,
|
|
505
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
506
|
+
});
|
|
507
|
+
const commands = [
|
|
508
|
+
{
|
|
509
|
+
name: "status",
|
|
510
|
+
description: "Show aggregated Second Nature status",
|
|
511
|
+
execute: async () => buildStatusPayload(spine),
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "policy",
|
|
515
|
+
description: "Write or inspect policy state",
|
|
516
|
+
execute: async (input) => {
|
|
517
|
+
const action = typeof input?.action === "string" ? input.action : "show";
|
|
518
|
+
if (action === "set") {
|
|
519
|
+
return createUnavailableActionError("HOST_SAFE_POLICY_SET_UNAVAILABLE", "policy set is unavailable in the host-safe plugin package", ["social_daily_limit", "quiet_enabled"], "run_workspace_runtime_or_reinstall_full_build");
|
|
520
|
+
}
|
|
521
|
+
return notImplemented("policy");
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "credential",
|
|
526
|
+
description: "Inspect or recover credential state",
|
|
527
|
+
execute: async (input) => {
|
|
528
|
+
const action = typeof input?.action === "string" ? input.action : "show";
|
|
529
|
+
if (action === "verify") {
|
|
530
|
+
return createUnavailableActionError("HOST_SAFE_CREDENTIAL_VERIFY_UNAVAILABLE", "credential verify is unavailable in the host-safe plugin package", ["verification_answer"], "run_workspace_runtime_or_reinstall_full_build");
|
|
531
|
+
}
|
|
532
|
+
const platformId = typeof input?.platformId === "string" ? input.platformId : undefined;
|
|
533
|
+
return buildCredentialPayload(spine, platformId);
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: "quiet",
|
|
538
|
+
description: "Inspect Quiet lifecycle state",
|
|
539
|
+
execute: async (input) => {
|
|
540
|
+
const scope = typeof input?.scope === "string" ? input.scope : undefined;
|
|
541
|
+
return buildQuietPayload(spine, scope);
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "report",
|
|
546
|
+
description: "Show daily report artifacts",
|
|
547
|
+
execute: async (input) => {
|
|
548
|
+
const day = typeof input?.day === "string" ? input.day : undefined;
|
|
549
|
+
return buildReportPayload(spine, day);
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: "session",
|
|
554
|
+
description: "Inspect continuity session details",
|
|
555
|
+
execute: async (input) => {
|
|
556
|
+
const sessionId = typeof input?.sessionId === "string" ? input.sessionId : undefined;
|
|
557
|
+
return buildSessionPayload(spine, sessionId);
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "audit",
|
|
562
|
+
description: "Inspect audit and evidence views",
|
|
563
|
+
execute: async () => notImplemented("audit"),
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: "explain",
|
|
567
|
+
description: "Answer why-question explain requests",
|
|
568
|
+
execute: async (input) => {
|
|
569
|
+
const subject = typeof input?.subject === "string" ? input.subject : undefined;
|
|
570
|
+
return buildExplainPayload(spine, subject);
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: "heartbeat_check",
|
|
575
|
+
description: "Acknowledge the shipping heartbeat bridge round",
|
|
576
|
+
execute: async (input) => buildHeartbeatCheckPayload(spine, input),
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "fallback",
|
|
580
|
+
description: "Operator-visible delivery fallback view (full workspace runtime required)",
|
|
581
|
+
execute: async (input) => {
|
|
582
|
+
const ref = typeof input?.ref === "string" ? input.ref.trim() : undefined;
|
|
583
|
+
return buildFallbackHostSafePayload(ref);
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: "storage_smoke",
|
|
588
|
+
description: "T4.1.4 storage mode smoke report (sql.js vs native probe)",
|
|
589
|
+
execute: async (input) => buildStorageSmokePayload(input),
|
|
590
|
+
},
|
|
591
|
+
];
|
|
592
|
+
return {
|
|
593
|
+
commands,
|
|
594
|
+
resolve(name) {
|
|
595
|
+
return commands.find((command) => command.name === name);
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function createActivationSpine() {
|
|
600
|
+
const workspaceRootContext = resolveWorkspaceRoot(undefined);
|
|
601
|
+
const spine = {
|
|
602
|
+
router: undefined,
|
|
603
|
+
runtimeHandle: startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot }),
|
|
604
|
+
lifecycleState: getLifecycleState(),
|
|
605
|
+
serviceStartRecorded: false,
|
|
606
|
+
runtimeEvidence: [],
|
|
607
|
+
workspaceRootContext,
|
|
608
|
+
};
|
|
609
|
+
spine.router = createHostSafeRouter(spine);
|
|
610
|
+
return spine;
|
|
611
|
+
}
|
|
612
|
+
function ensureActivationSpine() {
|
|
613
|
+
if (activationSpine) {
|
|
614
|
+
return activationSpine;
|
|
615
|
+
}
|
|
616
|
+
activationSpine = createActivationSpine();
|
|
617
|
+
return activationSpine;
|
|
618
|
+
}
|
|
619
|
+
function recordRuntimeEvidence(spine, origin) {
|
|
620
|
+
if (origin === "service_start" && spine.serviceStartRecorded) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (origin === "service_start") {
|
|
624
|
+
spine.serviceStartRecorded = true;
|
|
625
|
+
}
|
|
626
|
+
spine.runtimeEvidence.push({
|
|
627
|
+
traceId: `${INTERNAL_RUNTIME_TRACE_PREFIX}${origin}-${spine.lifecycleState.registerCount}-${Date.now()}`,
|
|
628
|
+
capability: origin === "register"
|
|
629
|
+
? spine.lifecycleState.registerCount === 1
|
|
630
|
+
? "runtime.activate"
|
|
631
|
+
: "runtime.reload"
|
|
632
|
+
: "runtime.heartbeat",
|
|
633
|
+
origin,
|
|
634
|
+
createdAt: new Date().toISOString(),
|
|
635
|
+
status: "succeeded",
|
|
636
|
+
});
|
|
637
|
+
trimRuntimeEvidence(spine);
|
|
638
|
+
}
|
|
639
|
+
function refreshRegistrationState() {
|
|
640
|
+
const spine = ensureActivationSpine();
|
|
641
|
+
const workspaceRootContext = resolveWorkspaceRoot(undefined);
|
|
642
|
+
const prev = spine.workspaceRootContext;
|
|
643
|
+
const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot || workspaceRootContext.resolution !== prev.resolution;
|
|
644
|
+
if (changed) {
|
|
645
|
+
disposeWorkspaceOpsBridge();
|
|
646
|
+
}
|
|
647
|
+
spine.workspaceRootContext = workspaceRootContext;
|
|
648
|
+
spine.runtimeHandle = startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot });
|
|
649
|
+
spine.lifecycleState = recordRegistration();
|
|
650
|
+
spine.serviceStartRecorded = false;
|
|
651
|
+
recordRuntimeEvidence(spine, "register");
|
|
652
|
+
return spine;
|
|
653
|
+
}
|
|
654
|
+
function parseCommandInput(rawArgs) {
|
|
655
|
+
const tokens = rawArgs?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
656
|
+
if (tokens.length === 0) {
|
|
657
|
+
return {
|
|
658
|
+
ok: false,
|
|
659
|
+
result: { ok: false, message: "Missing command argument." },
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const [command, ...rest] = tokens;
|
|
663
|
+
if (command === "policy" && rest[0] === "set") {
|
|
664
|
+
return {
|
|
665
|
+
ok: false,
|
|
666
|
+
result: {
|
|
667
|
+
ok: false,
|
|
668
|
+
command,
|
|
669
|
+
message: "policy set requires structured args; use second_nature_ops instead.",
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
if (command === "credential" && rest[0] === "verify") {
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
result: {
|
|
677
|
+
ok: false,
|
|
678
|
+
command,
|
|
679
|
+
message: "credential verify requires structured args; use second_nature_ops instead.",
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
switch (command) {
|
|
684
|
+
case "status":
|
|
685
|
+
case "quiet":
|
|
686
|
+
return {
|
|
687
|
+
ok: true,
|
|
688
|
+
command,
|
|
689
|
+
input: rest.length > 0 ? { scope: rest.join(" ") } : undefined,
|
|
690
|
+
};
|
|
691
|
+
case "report":
|
|
692
|
+
return {
|
|
693
|
+
ok: true,
|
|
694
|
+
command,
|
|
695
|
+
input: rest[0] ? { day: rest[0] } : undefined,
|
|
696
|
+
};
|
|
697
|
+
case "session":
|
|
698
|
+
return {
|
|
699
|
+
ok: true,
|
|
700
|
+
command,
|
|
701
|
+
input: rest[0] ? { sessionId: rest[0] } : undefined,
|
|
702
|
+
};
|
|
703
|
+
case "credential":
|
|
704
|
+
return {
|
|
705
|
+
ok: true,
|
|
706
|
+
command,
|
|
707
|
+
input: rest[0] ? { platformId: rest[0] } : undefined,
|
|
708
|
+
};
|
|
709
|
+
case "heartbeat_check":
|
|
710
|
+
return {
|
|
711
|
+
ok: true,
|
|
712
|
+
command,
|
|
713
|
+
input: rest.length > 0
|
|
714
|
+
? {
|
|
715
|
+
timestamp: rest[0],
|
|
716
|
+
sessionContext: rest.length > 1 ? rest.slice(1).join(" ") : undefined,
|
|
717
|
+
}
|
|
718
|
+
: undefined,
|
|
719
|
+
};
|
|
720
|
+
case "explain":
|
|
721
|
+
return {
|
|
722
|
+
ok: true,
|
|
723
|
+
command,
|
|
724
|
+
input: rest.length > 0 ? { subject: rest.join(" ") } : undefined,
|
|
725
|
+
};
|
|
726
|
+
case "fallback":
|
|
727
|
+
return {
|
|
728
|
+
ok: true,
|
|
729
|
+
command,
|
|
730
|
+
input: rest.length > 0 ? { ref: rest.join(" ") } : undefined,
|
|
731
|
+
};
|
|
732
|
+
case "storage_smoke": {
|
|
733
|
+
const wantRepair = rest[0] === "repair" || rest.includes("--repair");
|
|
734
|
+
return {
|
|
735
|
+
ok: true,
|
|
736
|
+
command,
|
|
737
|
+
input: wantRepair ? { runRepairFixture: true } : undefined,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
default:
|
|
741
|
+
return {
|
|
742
|
+
ok: true,
|
|
743
|
+
command,
|
|
744
|
+
input: undefined,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function createRuntimeService() {
|
|
749
|
+
return {
|
|
750
|
+
id: "second-nature-runtime",
|
|
751
|
+
start() {
|
|
752
|
+
const spine = ensureActivationSpine();
|
|
753
|
+
recordRuntimeEvidence(spine, "service_start");
|
|
754
|
+
return {
|
|
755
|
+
ready: spine.runtimeHandle.ready,
|
|
756
|
+
version: spine.runtimeHandle.version,
|
|
757
|
+
};
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function createLifecycleService() {
|
|
762
|
+
return {
|
|
763
|
+
id: "second-nature-lifecycle",
|
|
764
|
+
start() {
|
|
765
|
+
const spine = ensureActivationSpine();
|
|
766
|
+
return {
|
|
767
|
+
phase: spine.lifecycleState.phase,
|
|
768
|
+
registerCount: spine.lifecycleState.registerCount,
|
|
769
|
+
lastChangedAt: spine.lifecycleState.lastChangedAt,
|
|
770
|
+
};
|
|
771
|
+
},
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const SECOND_NATURE_TOOL_SCHEMA = {
|
|
775
|
+
type: "object",
|
|
776
|
+
additionalProperties: false,
|
|
777
|
+
properties: {
|
|
778
|
+
command: { type: "string" },
|
|
779
|
+
args: { type: "object", additionalProperties: true },
|
|
780
|
+
workspaceRoot: {
|
|
781
|
+
type: "string",
|
|
782
|
+
description: "Workspace root for packaged smoke/runtime alignment (optional; prefer SECOND_NATURE_WORKSPACE_ROOT).",
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
required: ["command"],
|
|
786
|
+
};
|
|
787
|
+
export default definePluginEntry({
|
|
788
|
+
id: "second-nature",
|
|
789
|
+
name: "Second Nature",
|
|
790
|
+
description: "Registers command/tool/service surface with load-reload lifecycle semantics.",
|
|
791
|
+
register(api) {
|
|
792
|
+
process.stderr.write(`[second-nature] register() entered, api keys=${Object.keys(api).join(",")}\n`);
|
|
793
|
+
const runtimeService = createRuntimeService();
|
|
794
|
+
const lifecycleService = createLifecycleService();
|
|
795
|
+
api.registerService(runtimeService);
|
|
796
|
+
api.registerService(lifecycleService);
|
|
797
|
+
api.registerCommand({
|
|
798
|
+
name: "second-nature",
|
|
799
|
+
description: "Route Agent-facing operational commands for Second Nature.",
|
|
800
|
+
acceptsArgs: true,
|
|
801
|
+
handler: async (ctx) => {
|
|
802
|
+
const spine = ensureActivationSpine();
|
|
803
|
+
const parsed = parseCommandInput(ctx.args);
|
|
804
|
+
if (!parsed.ok) {
|
|
805
|
+
return {
|
|
806
|
+
text: JSON.stringify(parsed.result),
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
const resolved = spine.router.resolve(parsed.command);
|
|
810
|
+
if (!resolved) {
|
|
811
|
+
return {
|
|
812
|
+
text: JSON.stringify({ ok: false, command: parsed.command, message: "Unknown Second Nature command." }),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
|
|
816
|
+
return {
|
|
817
|
+
text: JSON.stringify(result),
|
|
818
|
+
};
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
const executeSecondNatureTool = async (params) => {
|
|
822
|
+
const spine = ensureActivationSpine();
|
|
823
|
+
syncWorkspaceRootFromTool(spine, params.workspaceRoot);
|
|
824
|
+
const resolved = spine.router.resolve(params.command);
|
|
825
|
+
if (!resolved) {
|
|
826
|
+
return {
|
|
827
|
+
content: [
|
|
828
|
+
{
|
|
829
|
+
type: "text",
|
|
830
|
+
text: JSON.stringify({ ok: false, message: "Unknown Second Nature command." }),
|
|
831
|
+
},
|
|
832
|
+
],
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
const result = await routeSecondNatureCommand(spine, params.command, params.args);
|
|
836
|
+
return {
|
|
837
|
+
content: [
|
|
838
|
+
{
|
|
839
|
+
type: "text",
|
|
840
|
+
text: JSON.stringify(result),
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
};
|
|
844
|
+
};
|
|
845
|
+
api.registerTool({
|
|
846
|
+
name: "second_nature_ops",
|
|
847
|
+
description: "Access the Second Nature command surface through a single tool shell.",
|
|
848
|
+
parameters: SECOND_NATURE_TOOL_SCHEMA,
|
|
849
|
+
async execute(_id, params) {
|
|
850
|
+
return executeSecondNatureTool(params);
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
process.stderr.write("[second-nature] register() completed\n");
|
|
854
|
+
},
|
|
855
|
+
});
|