@haaaiawd/second-nature 0.1.9 → 0.1.11
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 +250 -68
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -1
- package/runtime/cli/ops/heartbeat-surface.js +2 -2
- package/runtime/cli/ops/ops-router.js +6 -2
- package/runtime/core/second-nature/runtime/service-entry.js +2 -1
- package/runtime/guidance/outreach-draft-schema.d.ts +3 -3
- package/runtime/observability/services/lived-experience-audit.js +2 -1
- package/runtime/storage/repositories/credential-repository.js +12 -1
- package/runtime/storage/services/credential-vault.js +4 -3
- package/workspace-ops-bridge.js +78 -0
package/index.js
CHANGED
|
@@ -16,15 +16,111 @@
|
|
|
16
16
|
* - structured mutating flows such as policy set / credential verify remain unavailable here
|
|
17
17
|
* - full evidence-backed workspace runtime can be reintroduced later behind a host-safe boundary
|
|
18
18
|
*
|
|
19
|
+
* OpenClaw operator norm (T1.1.4 / T1.1.5): set `SECOND_NATURE_WORKSPACE_ROOT` or tool `workspaceRoot` to the
|
|
20
|
+
* **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
|
|
21
|
+
* `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
|
|
22
|
+
* install directory. With **sandbox** or **per-agent workspaces**, use the path where `data/state.db` and
|
|
23
|
+
* `workspace/` anchors actually live. See `explore/reports/2026-05-04_openclaw-plugin-install-vs-workspace-root.md`.
|
|
24
|
+
*
|
|
19
25
|
* Test coverage:
|
|
20
26
|
* - tests/integration/cli/plugin-runtime-registration.test.ts
|
|
21
27
|
* - tests/integration/cli/plugin-packaging-walkthrough.test.ts
|
|
28
|
+
* - tests/integration/cli/plugin-workspace-ops-bridge.test.ts (T1.1.4 / CH-13 matrix, T1.1.5 ops docs cross-ref)
|
|
22
29
|
*/
|
|
23
30
|
import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
|
|
24
31
|
import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
|
|
32
|
+
import { openWorkspaceOpsBridge } from "./workspace-ops-bridge.js";
|
|
25
33
|
const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
|
|
26
34
|
const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
|
|
27
35
|
let activationSpine = null;
|
|
36
|
+
/** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
|
|
37
|
+
let workspaceOpsBridge = null;
|
|
38
|
+
function disposeWorkspaceOpsBridge() {
|
|
39
|
+
if (workspaceOpsBridge) {
|
|
40
|
+
workspaceOpsBridge.close();
|
|
41
|
+
workspaceOpsBridge = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const WORKSPACE_BRIDGE_COMMANDS = new Set([
|
|
45
|
+
"status",
|
|
46
|
+
"quiet",
|
|
47
|
+
"report",
|
|
48
|
+
"session",
|
|
49
|
+
"explain",
|
|
50
|
+
"heartbeat_check",
|
|
51
|
+
"fallback",
|
|
52
|
+
"storage_smoke",
|
|
53
|
+
]);
|
|
54
|
+
function isWorkspaceBridgeCommand(command, input) {
|
|
55
|
+
if (command === "credential") {
|
|
56
|
+
const action = typeof input?.action === "string" ? input.action : "show";
|
|
57
|
+
return action !== "verify";
|
|
58
|
+
}
|
|
59
|
+
return WORKSPACE_BRIDGE_COMMANDS.has(command);
|
|
60
|
+
}
|
|
61
|
+
async function ensureWorkspaceOpsBridge(spine) {
|
|
62
|
+
const root = spine.workspaceRootContext.runtimeRoot;
|
|
63
|
+
if (workspaceOpsBridge?.root === root) {
|
|
64
|
+
return { ok: true, dispatch: workspaceOpsBridge.dispatch };
|
|
65
|
+
}
|
|
66
|
+
disposeWorkspaceOpsBridge();
|
|
67
|
+
const opened = await openWorkspaceOpsBridge(root);
|
|
68
|
+
if (!opened.ok) {
|
|
69
|
+
return opened;
|
|
70
|
+
}
|
|
71
|
+
workspaceOpsBridge = { root, close: opened.close, dispatch: opened.dispatch };
|
|
72
|
+
return { ok: true, dispatch: opened.dispatch };
|
|
73
|
+
}
|
|
74
|
+
async function routeSecondNatureCommand(spine, command, input) {
|
|
75
|
+
const wr = spine.workspaceRootContext;
|
|
76
|
+
const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
|
|
77
|
+
if (useBridge) {
|
|
78
|
+
const bridge = await ensureWorkspaceOpsBridge(spine);
|
|
79
|
+
if (!bridge.ok) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
surfaceMode: "host_safe_carrier",
|
|
83
|
+
workspaceReadModelsEvaluated: false,
|
|
84
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
85
|
+
error: bridge.error,
|
|
86
|
+
data: {
|
|
87
|
+
workspaceRootResolution: wr.resolution,
|
|
88
|
+
bridgeAttempted: true,
|
|
89
|
+
declaredRoot: wr.declaredRoot,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return (await bridge.dispatch(command, input));
|
|
94
|
+
}
|
|
95
|
+
const def = spine.router.resolve(command);
|
|
96
|
+
if (!def) {
|
|
97
|
+
return { ok: false, message: `Unknown Second Nature command: ${command}` };
|
|
98
|
+
}
|
|
99
|
+
return def.execute(input);
|
|
100
|
+
}
|
|
101
|
+
function resolveWorkspaceRoot(toolWorkspaceRoot) {
|
|
102
|
+
const env = process.env.SECOND_NATURE_WORKSPACE_ROOT?.trim();
|
|
103
|
+
if (env) {
|
|
104
|
+
return { resolution: "env", declaredRoot: env, runtimeRoot: env };
|
|
105
|
+
}
|
|
106
|
+
const tool = toolWorkspaceRoot?.trim();
|
|
107
|
+
if (tool) {
|
|
108
|
+
return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
|
|
109
|
+
}
|
|
110
|
+
return { resolution: "unknown", declaredRoot: undefined, runtimeRoot: process.cwd() };
|
|
111
|
+
}
|
|
112
|
+
function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
|
|
113
|
+
const next = resolveWorkspaceRoot(toolWorkspaceRoot);
|
|
114
|
+
const prev = spine.workspaceRootContext;
|
|
115
|
+
const changed = next.runtimeRoot !== prev.runtimeRoot || next.resolution !== prev.resolution;
|
|
116
|
+
if (changed) {
|
|
117
|
+
disposeWorkspaceOpsBridge();
|
|
118
|
+
}
|
|
119
|
+
spine.workspaceRootContext = next;
|
|
120
|
+
if (changed) {
|
|
121
|
+
spine.runtimeHandle = startRuntimeService({ workspaceRoot: next.runtimeRoot });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
28
124
|
function trimRuntimeEvidence(spine) {
|
|
29
125
|
if (spine.runtimeEvidence.length > 12) {
|
|
30
126
|
spine.runtimeEvidence.splice(0, spine.runtimeEvidence.length - 12);
|
|
@@ -88,56 +184,73 @@ function parseExplainSubject(subjectRaw) {
|
|
|
88
184
|
function buildStatusPayload(spine) {
|
|
89
185
|
const runtimeEvidence = latestRuntimeEvidence(spine);
|
|
90
186
|
const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
|
|
187
|
+
const wr = spine.workspaceRootContext;
|
|
188
|
+
const needsRootHint = wr.resolution === "unknown";
|
|
91
189
|
return {
|
|
92
|
-
ok:
|
|
190
|
+
ok: false,
|
|
191
|
+
surfaceMode: "host_safe_carrier",
|
|
192
|
+
workspaceReadModelsEvaluated: false,
|
|
193
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
194
|
+
error: {
|
|
195
|
+
code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
|
|
196
|
+
message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
|
|
197
|
+
requiredUserInput: needsRootHint ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
198
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
199
|
+
},
|
|
93
200
|
data: {
|
|
94
|
-
|
|
201
|
+
workspaceRootResolution: wr.resolution,
|
|
202
|
+
carrier: {
|
|
95
203
|
host: "openclaw-plugin",
|
|
96
204
|
serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
|
|
97
205
|
updatedAt,
|
|
98
|
-
|
|
99
|
-
rhythm: {
|
|
100
|
-
mode: "active",
|
|
101
|
-
windowId: undefined,
|
|
102
|
-
},
|
|
103
|
-
quiet: {
|
|
104
|
-
mode: "unknown",
|
|
105
|
-
lastEvent: runtimeEvidence?.traceId,
|
|
106
|
-
interrupted: undefined,
|
|
107
|
-
},
|
|
108
|
-
connectors: [],
|
|
109
|
-
credentials: [],
|
|
110
|
-
risk: {
|
|
111
|
-
level: "low",
|
|
112
|
-
flags: [],
|
|
206
|
+
lastRuntimeTraceId: runtimeEvidence?.traceId,
|
|
113
207
|
},
|
|
114
208
|
},
|
|
115
209
|
};
|
|
116
210
|
}
|
|
117
|
-
function buildQuietPayload(scope) {
|
|
211
|
+
function buildQuietPayload(spine, scope) {
|
|
212
|
+
const wr = spine.workspaceRootContext;
|
|
118
213
|
return {
|
|
119
|
-
ok:
|
|
214
|
+
ok: false,
|
|
215
|
+
surfaceMode: "host_safe_carrier",
|
|
216
|
+
workspaceReadModelsEvaluated: false,
|
|
217
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
218
|
+
error: {
|
|
219
|
+
code: "QUIET_READ_SURFACE_UNAVAILABLE",
|
|
220
|
+
message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
|
|
221
|
+
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
222
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
223
|
+
},
|
|
120
224
|
data: {
|
|
121
225
|
scope,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
recentJournalCount: 0,
|
|
226
|
+
evaluated: false,
|
|
227
|
+
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
228
|
+
workspaceRootResolution: wr.resolution,
|
|
126
229
|
},
|
|
127
230
|
};
|
|
128
231
|
}
|
|
129
|
-
function buildReportPayload(day) {
|
|
232
|
+
function buildReportPayload(spine, day) {
|
|
233
|
+
const wr = spine.workspaceRootContext;
|
|
130
234
|
return {
|
|
131
|
-
ok:
|
|
235
|
+
ok: false,
|
|
236
|
+
surfaceMode: "host_safe_carrier",
|
|
237
|
+
workspaceReadModelsEvaluated: false,
|
|
238
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
239
|
+
error: {
|
|
240
|
+
code: "REPORT_READ_SURFACE_UNAVAILABLE",
|
|
241
|
+
message: "Daily report artifacts require workspace runtime.",
|
|
242
|
+
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
243
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
244
|
+
},
|
|
132
245
|
data: {
|
|
246
|
+
evaluated: false,
|
|
247
|
+
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
133
248
|
day: day && day.trim() ? day : new Date().toISOString().slice(0, 10),
|
|
134
|
-
|
|
135
|
-
highlights: [],
|
|
136
|
-
sourceRefs: [],
|
|
249
|
+
workspaceRootResolution: wr.resolution,
|
|
137
250
|
},
|
|
138
251
|
};
|
|
139
252
|
}
|
|
140
|
-
function buildSessionPayload(sessionId) {
|
|
253
|
+
function buildSessionPayload(spine, sessionId) {
|
|
141
254
|
if (!sessionId) {
|
|
142
255
|
return {
|
|
143
256
|
ok: false,
|
|
@@ -149,26 +262,44 @@ function buildSessionPayload(sessionId) {
|
|
|
149
262
|
},
|
|
150
263
|
};
|
|
151
264
|
}
|
|
265
|
+
const wr = spine.workspaceRootContext;
|
|
152
266
|
return {
|
|
153
|
-
ok:
|
|
267
|
+
ok: false,
|
|
268
|
+
surfaceMode: "host_safe_carrier",
|
|
269
|
+
workspaceReadModelsEvaluated: false,
|
|
270
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
271
|
+
error: {
|
|
272
|
+
code: "SESSION_READ_SURFACE_UNAVAILABLE",
|
|
273
|
+
message: "Session analytics require workspace state database.",
|
|
274
|
+
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
275
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
276
|
+
},
|
|
154
277
|
data: {
|
|
155
278
|
requestedSessionId: sessionId,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
governanceCount: 0,
|
|
160
|
-
keyFactors: [],
|
|
161
|
-
evidenceRefs: [],
|
|
279
|
+
evaluated: false,
|
|
280
|
+
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
281
|
+
workspaceRootResolution: wr.resolution,
|
|
162
282
|
},
|
|
163
283
|
};
|
|
164
284
|
}
|
|
165
|
-
function buildCredentialPayload(platformId) {
|
|
285
|
+
function buildCredentialPayload(spine, platformId) {
|
|
286
|
+
const wr = spine.workspaceRootContext;
|
|
166
287
|
return {
|
|
167
|
-
ok:
|
|
288
|
+
ok: false,
|
|
289
|
+
surfaceMode: "host_safe_carrier",
|
|
290
|
+
workspaceReadModelsEvaluated: false,
|
|
291
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
292
|
+
error: {
|
|
293
|
+
code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
|
|
294
|
+
message: "Credential inspection requires workspace runtime on this surface.",
|
|
295
|
+
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
296
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
297
|
+
},
|
|
168
298
|
data: {
|
|
169
|
-
platformId: platformId && platformId.trim() ? platformId :
|
|
170
|
-
|
|
171
|
-
|
|
299
|
+
platformId: platformId && platformId.trim() ? platformId : undefined,
|
|
300
|
+
evaluated: false,
|
|
301
|
+
unavailableReason: "host_safe_carrier_no_workspace_db",
|
|
302
|
+
workspaceRootResolution: wr.resolution,
|
|
172
303
|
},
|
|
173
304
|
};
|
|
174
305
|
}
|
|
@@ -198,23 +329,22 @@ function buildExplainPayload(spine, subjectRaw) {
|
|
|
198
329
|
}
|
|
199
330
|
return createUnavailableActionError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject", ["subject"], "reinvoke_explain_with_supported_subject");
|
|
200
331
|
}
|
|
201
|
-
const
|
|
332
|
+
const wr = spine.workspaceRootContext;
|
|
202
333
|
return {
|
|
203
|
-
ok:
|
|
334
|
+
ok: false,
|
|
335
|
+
surfaceMode: "host_safe_carrier",
|
|
336
|
+
workspaceReadModelsEvaluated: false,
|
|
337
|
+
message: HOST_SAFE_LIMITATION_MESSAGE,
|
|
338
|
+
error: {
|
|
339
|
+
code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
|
|
340
|
+
message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
|
|
341
|
+
requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
|
|
342
|
+
nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
|
|
343
|
+
},
|
|
204
344
|
data: {
|
|
205
345
|
subjectType: subject.subjectType,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
"synchronous_register",
|
|
209
|
-
`subject:${subject.subjectId}`,
|
|
210
|
-
runtimeEvidence?.capability ?? "runtime.activate",
|
|
211
|
-
],
|
|
212
|
-
evidenceRefs: [
|
|
213
|
-
runtimeEvidence?.traceId ?? `${INTERNAL_RUNTIME_TRACE_PREFIX}none`,
|
|
214
|
-
`subject:${subjectRaw.trim()}`,
|
|
215
|
-
"host_safe_mode",
|
|
216
|
-
],
|
|
217
|
-
nextStep: "use full workspace runtime for evidence-backed explain details",
|
|
346
|
+
evaluated: false,
|
|
347
|
+
workspaceRootResolution: wr.resolution,
|
|
218
348
|
},
|
|
219
349
|
};
|
|
220
350
|
}
|
|
@@ -252,20 +382,58 @@ function buildFallbackHostSafePayload(ref) {
|
|
|
252
382
|
}
|
|
253
383
|
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");
|
|
254
384
|
}
|
|
385
|
+
function isProbeOnlyInput(input) {
|
|
386
|
+
const v = input?.probeOnly;
|
|
387
|
+
return v === true || v === "true" || v === 1 || v === "1";
|
|
388
|
+
}
|
|
255
389
|
function buildHeartbeatCheckPayload(spine, input) {
|
|
256
390
|
const runtimeEvidence = latestRuntimeEvidence(spine);
|
|
257
391
|
const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
|
|
258
392
|
const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0 ? input.timestamp : updatedAt;
|
|
393
|
+
const wr = spine.workspaceRootContext;
|
|
394
|
+
if (isProbeOnlyInput(input)) {
|
|
395
|
+
return {
|
|
396
|
+
ok: true,
|
|
397
|
+
status: "heartbeat_ok",
|
|
398
|
+
surfaceMode: "capability_probe",
|
|
399
|
+
reasons: ["probe_only"],
|
|
400
|
+
livedExperienceLoopClaimed: false,
|
|
401
|
+
scope: "rhythm",
|
|
402
|
+
trigger: "heartbeat_bridge",
|
|
403
|
+
message: "Capability probe only on the host-safe carrier surface; does not claim a full lived-experience decision loop.",
|
|
404
|
+
data: {
|
|
405
|
+
workspaceRootResolution: wr.resolution,
|
|
406
|
+
runtime: {
|
|
407
|
+
host: "openclaw-plugin",
|
|
408
|
+
serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
|
|
409
|
+
updatedAt,
|
|
410
|
+
},
|
|
411
|
+
surface: {
|
|
412
|
+
tool: "second_nature_ops",
|
|
413
|
+
command: "second-nature heartbeat_check",
|
|
414
|
+
},
|
|
415
|
+
bridge: {
|
|
416
|
+
timestamp,
|
|
417
|
+
probeOnly: true,
|
|
418
|
+
sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
|
|
419
|
+
heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
|
|
420
|
+
serviceEntryMode: "capability_probe",
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
259
425
|
return {
|
|
260
426
|
ok: true,
|
|
261
|
-
status: "
|
|
262
|
-
|
|
427
|
+
status: "runtime_carrier_only",
|
|
428
|
+
surfaceMode: "host_safe_carrier",
|
|
429
|
+
livedExperienceLoopClaimed: false,
|
|
263
430
|
scope: "rhythm",
|
|
264
431
|
trigger: "heartbeat_bridge",
|
|
265
|
-
reasons: ["
|
|
266
|
-
nextAction: "
|
|
267
|
-
message: "
|
|
432
|
+
reasons: ["runtime_carrier_only", "host_safe_bridge_ack"],
|
|
433
|
+
nextAction: "continue_carrier_surface_only",
|
|
434
|
+
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.",
|
|
268
435
|
data: {
|
|
436
|
+
workspaceRootResolution: wr.resolution,
|
|
269
437
|
runtime: {
|
|
270
438
|
host: "openclaw-plugin",
|
|
271
439
|
serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
|
|
@@ -316,7 +484,7 @@ function createHostSafeRouter(spine) {
|
|
|
316
484
|
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");
|
|
317
485
|
}
|
|
318
486
|
const platformId = typeof input?.platformId === "string" ? input.platformId : undefined;
|
|
319
|
-
return buildCredentialPayload(platformId);
|
|
487
|
+
return buildCredentialPayload(spine, platformId);
|
|
320
488
|
},
|
|
321
489
|
},
|
|
322
490
|
{
|
|
@@ -324,7 +492,7 @@ function createHostSafeRouter(spine) {
|
|
|
324
492
|
description: "Inspect Quiet lifecycle state",
|
|
325
493
|
execute: async (input) => {
|
|
326
494
|
const scope = typeof input?.scope === "string" ? input.scope : undefined;
|
|
327
|
-
return buildQuietPayload(scope);
|
|
495
|
+
return buildQuietPayload(spine, scope);
|
|
328
496
|
},
|
|
329
497
|
},
|
|
330
498
|
{
|
|
@@ -332,7 +500,7 @@ function createHostSafeRouter(spine) {
|
|
|
332
500
|
description: "Show daily report artifacts",
|
|
333
501
|
execute: async (input) => {
|
|
334
502
|
const day = typeof input?.day === "string" ? input.day : undefined;
|
|
335
|
-
return buildReportPayload(day);
|
|
503
|
+
return buildReportPayload(spine, day);
|
|
336
504
|
},
|
|
337
505
|
},
|
|
338
506
|
{
|
|
@@ -340,7 +508,7 @@ function createHostSafeRouter(spine) {
|
|
|
340
508
|
description: "Inspect continuity session details",
|
|
341
509
|
execute: async (input) => {
|
|
342
510
|
const sessionId = typeof input?.sessionId === "string" ? input.sessionId : undefined;
|
|
343
|
-
return buildSessionPayload(sessionId);
|
|
511
|
+
return buildSessionPayload(spine, sessionId);
|
|
344
512
|
},
|
|
345
513
|
},
|
|
346
514
|
{
|
|
@@ -383,12 +551,14 @@ function createHostSafeRouter(spine) {
|
|
|
383
551
|
};
|
|
384
552
|
}
|
|
385
553
|
function createActivationSpine() {
|
|
554
|
+
const workspaceRootContext = resolveWorkspaceRoot(undefined);
|
|
386
555
|
const spine = {
|
|
387
556
|
router: undefined,
|
|
388
|
-
runtimeHandle: startRuntimeService({ workspaceRoot:
|
|
557
|
+
runtimeHandle: startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot }),
|
|
389
558
|
lifecycleState: getLifecycleState(),
|
|
390
559
|
serviceStartRecorded: false,
|
|
391
560
|
runtimeEvidence: [],
|
|
561
|
+
workspaceRootContext,
|
|
392
562
|
};
|
|
393
563
|
spine.router = createHostSafeRouter(spine);
|
|
394
564
|
return spine;
|
|
@@ -422,7 +592,14 @@ function recordRuntimeEvidence(spine, origin) {
|
|
|
422
592
|
}
|
|
423
593
|
function refreshRegistrationState() {
|
|
424
594
|
const spine = ensureActivationSpine();
|
|
425
|
-
|
|
595
|
+
const workspaceRootContext = resolveWorkspaceRoot(undefined);
|
|
596
|
+
const prev = spine.workspaceRootContext;
|
|
597
|
+
const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot || workspaceRootContext.resolution !== prev.resolution;
|
|
598
|
+
if (changed) {
|
|
599
|
+
disposeWorkspaceOpsBridge();
|
|
600
|
+
}
|
|
601
|
+
spine.workspaceRootContext = workspaceRootContext;
|
|
602
|
+
spine.runtimeHandle = startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot });
|
|
426
603
|
spine.lifecycleState = recordRegistration();
|
|
427
604
|
spine.serviceStartRecorded = false;
|
|
428
605
|
recordRuntimeEvidence(spine, "register");
|
|
@@ -575,7 +752,7 @@ export default {
|
|
|
575
752
|
text: JSON.stringify({ ok: false, command: parsed.command, message: "Unknown Second Nature command." }),
|
|
576
753
|
};
|
|
577
754
|
}
|
|
578
|
-
const result = await
|
|
755
|
+
const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
|
|
579
756
|
return {
|
|
580
757
|
text: JSON.stringify(result),
|
|
581
758
|
};
|
|
@@ -590,11 +767,16 @@ export default {
|
|
|
590
767
|
properties: {
|
|
591
768
|
command: { type: "string" },
|
|
592
769
|
args: { type: "object", additionalProperties: true },
|
|
770
|
+
workspaceRoot: {
|
|
771
|
+
type: "string",
|
|
772
|
+
description: "Workspace root for packaged smoke/runtime alignment (optional; prefer SECOND_NATURE_WORKSPACE_ROOT).",
|
|
773
|
+
},
|
|
593
774
|
},
|
|
594
775
|
required: ["command"],
|
|
595
776
|
},
|
|
596
777
|
async execute(_id, params) {
|
|
597
778
|
const spine = ensureActivationSpine();
|
|
779
|
+
syncWorkspaceRootFromTool(spine, params.workspaceRoot);
|
|
598
780
|
const resolved = spine.router.resolve(params.command);
|
|
599
781
|
if (!resolved) {
|
|
600
782
|
return {
|
|
@@ -606,7 +788,7 @@ export default {
|
|
|
606
788
|
],
|
|
607
789
|
};
|
|
608
790
|
}
|
|
609
|
-
const result = await
|
|
791
|
+
const result = await routeSecondNatureCommand(spine, params.command, params.args);
|
|
610
792
|
return {
|
|
611
793
|
content: [
|
|
612
794
|
{
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.11",
|
|
5
5
|
"entry": "./index.js",
|
|
6
|
-
"description": "OpenClaw native plugin
|
|
6
|
+
"description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace (see README / T1.1.4 ops norm).",
|
|
7
7
|
"capabilities": {
|
|
8
8
|
"commands": [
|
|
9
9
|
"second-nature"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haaaiawd/second-nature",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "OpenClaw native plugin with synchronous registration, a packaged runtime artifact, and operator-facing status/explain flows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"main": "./index.js",
|
|
18
18
|
"files": [
|
|
19
19
|
"index.js",
|
|
20
|
+
"workspace-ops-bridge.js",
|
|
20
21
|
"openclaw.plugin.json",
|
|
21
22
|
"runtime/"
|
|
22
23
|
],
|
|
@@ -48,8 +48,8 @@ export async function heartbeatCheck(input) {
|
|
|
48
48
|
if (!input.readModels) {
|
|
49
49
|
return {
|
|
50
50
|
ok: true,
|
|
51
|
-
status: "
|
|
52
|
-
surfaceMode: "
|
|
51
|
+
status: "runtime_carrier_only",
|
|
52
|
+
surfaceMode: "host_safe_carrier",
|
|
53
53
|
reasons: ["heartbeat_read_models_unavailable"],
|
|
54
54
|
livedExperienceLoopClaimed: false,
|
|
55
55
|
};
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { heartbeatCheck } from "./heartbeat-surface.js";
|
|
5
5
|
import { showOperatorFallback, OperatorFallbackNotFoundError } from "./show-operator-fallback.js";
|
|
6
|
+
function coerceProbeOnlyFlag(input) {
|
|
7
|
+
const v = input?.probeOnly;
|
|
8
|
+
return v === true || v === "true" || v === 1 || v === "1";
|
|
9
|
+
}
|
|
6
10
|
export function createOpsRouter(deps) {
|
|
7
11
|
return {
|
|
8
12
|
heartbeatCheck: (input) => heartbeatCheck({
|
|
@@ -14,12 +18,12 @@ export function createOpsRouter(deps) {
|
|
|
14
18
|
if (command === "heartbeat_check") {
|
|
15
19
|
const runtimeAvailable = typeof input?.runtimeAvailable === "boolean" ? input.runtimeAvailable : deps.runtimeAvailable;
|
|
16
20
|
return heartbeatCheck({
|
|
17
|
-
probeOnly:
|
|
21
|
+
probeOnly: coerceProbeOnlyFlag(input),
|
|
18
22
|
runtimeAvailable,
|
|
19
23
|
fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough && typeof input.fakeControlPlanePassthrough === "object"
|
|
20
24
|
? input.fakeControlPlanePassthrough
|
|
21
25
|
: undefined,
|
|
22
|
-
readModels: deps.readModels,
|
|
26
|
+
readModels: input?.readModels ?? deps.readModels,
|
|
23
27
|
timestamp: typeof input?.timestamp === "string" ? input.timestamp : undefined,
|
|
24
28
|
sessionContext: typeof input?.sessionContext === "string" ? input.sessionContext : undefined,
|
|
25
29
|
scopeHint: input?.scopeHint,
|
|
@@ -26,7 +26,8 @@ export function startRuntimeService(ctx) {
|
|
|
26
26
|
// - observability-system (event store setup)
|
|
27
27
|
// - control-plane-system (heartbeat bridge preparation)
|
|
28
28
|
const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
|
|
29
|
-
|
|
29
|
+
/** Keep in sync with `plugin/package.json` when cutting releases. */
|
|
30
|
+
const version = "0.1.11";
|
|
30
31
|
activeHandle = {
|
|
31
32
|
ready: true,
|
|
32
33
|
version,
|
|
@@ -59,8 +59,8 @@ export declare const sceneGuidanceRequestSchema: z.ZodObject<{
|
|
|
59
59
|
maintenance: "maintenance";
|
|
60
60
|
}>>;
|
|
61
61
|
riskLevel: z.ZodEnum<{
|
|
62
|
-
low: "low";
|
|
63
62
|
medium: "medium";
|
|
63
|
+
low: "low";
|
|
64
64
|
high: "high";
|
|
65
65
|
}>;
|
|
66
66
|
sourceRefs: z.ZodArray<z.ZodObject<{
|
|
@@ -113,8 +113,8 @@ export declare const outreachDraftRequestSchema: z.ZodObject<{
|
|
|
113
113
|
maintenance: "maintenance";
|
|
114
114
|
}>>;
|
|
115
115
|
riskLevel: z.ZodEnum<{
|
|
116
|
-
low: "low";
|
|
117
116
|
medium: "medium";
|
|
117
|
+
low: "low";
|
|
118
118
|
high: "high";
|
|
119
119
|
}>;
|
|
120
120
|
sourceRefs: z.ZodArray<z.ZodObject<{
|
|
@@ -185,7 +185,7 @@ export declare function parseOutreachDraftRequest(input: unknown): OutreachDraft
|
|
|
185
185
|
export declare function safeParseOutreachDraftRequest(input: unknown): z.ZodSafeParseResult<{
|
|
186
186
|
requestId: string;
|
|
187
187
|
runtimeScope: "rhythm" | "user_reply" | "user_task";
|
|
188
|
-
riskLevel: "
|
|
188
|
+
riskLevel: "medium" | "low" | "high";
|
|
189
189
|
sourceRefs: {
|
|
190
190
|
id: string;
|
|
191
191
|
kind: "platform_item" | "workspace_artifact" | "decision_record" | "user_anchor" | "connector_result" | "host_report" | "fallback_artifact";
|
|
@@ -91,7 +91,8 @@ export class LivedExperienceAuditRecorder {
|
|
|
91
91
|
payload.status === "not_sent_fallback" ||
|
|
92
92
|
payload.status === "channel_missing" ||
|
|
93
93
|
payload.status === "host_unsupported" ||
|
|
94
|
-
payload.status === "failed"
|
|
94
|
+
payload.status === "failed" ||
|
|
95
|
+
payload.status === "ack_dropped") {
|
|
95
96
|
entry.noUserVisibleContact = true;
|
|
96
97
|
}
|
|
97
98
|
return { eventId: envelope.eventId };
|
|
@@ -12,8 +12,19 @@ export class CredentialRepository {
|
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
async findByPlatformId(platformId) {
|
|
15
|
-
|
|
15
|
+
const row = await this.database.db.query.credentialRecords.findFirst({
|
|
16
16
|
where: eq(credentialRecords.platformId, platformId),
|
|
17
17
|
});
|
|
18
|
+
if (row == null) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const r = row;
|
|
22
|
+
const pid = (r.platformId ?? r.platform_id);
|
|
23
|
+
const enc = (r.encryptedValue ?? r.encrypted_value);
|
|
24
|
+
// sql.js + Drizzle: no-match can still return a "shell" row (keys present, values undefined).
|
|
25
|
+
if (pid == null || pid === "" || enc == null || enc === "") {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return row;
|
|
18
29
|
}
|
|
19
30
|
}
|
|
@@ -90,9 +90,10 @@ export function createCredentialVault(db) {
|
|
|
90
90
|
return null;
|
|
91
91
|
let plain;
|
|
92
92
|
if (record.encryptedValue) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
if (!isCredentialCiphertext(record.encryptedValue)) {
|
|
94
|
+
throw new Error("credential_store_plaintext_or_invalid_legacy_record");
|
|
95
|
+
}
|
|
96
|
+
plain = decryptCredentialAtRest(record.encryptedValue);
|
|
96
97
|
}
|
|
97
98
|
return {
|
|
98
99
|
platformId: record.platformId,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T1.1.4 — Lazy workspace full-ops bridge (OpenClaw plugin).
|
|
3
|
+
*
|
|
4
|
+
* Core logic: dynamic-import packaged `runtime/` + open workspace `data/*.db` with the same
|
|
5
|
+
* `createCliRuntimeDeps` + `createOpsRouter` + `createCliCommands` path as the workspace CLI.
|
|
6
|
+
* `process.chdir(workspaceRoot)` during dispatch so `memory/workspace` paths match CLI cwd semantics.
|
|
7
|
+
*
|
|
8
|
+
* Boundaries: no static imports from `./runtime/*` (sql.js top-level await stays out of register() graph).
|
|
9
|
+
*
|
|
10
|
+
* Plan B (CH-11-01): if the host VM blocks dynamic import + sql.js, fall back to a subprocess invoking
|
|
11
|
+
* the workspace `second-nature` CLI — not implemented here; bridge failures surface as explicit errors.
|
|
12
|
+
*
|
|
13
|
+
* Test coverage: tests/integration/cli/plugin-workspace-ops-bridge.test.ts
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
const PLUGIN_PACKAGE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
export async function openWorkspaceOpsBridge(workspaceRoot) {
|
|
20
|
+
const resolvedRoot = path.resolve(workspaceRoot);
|
|
21
|
+
try {
|
|
22
|
+
// Packaged `plugin/runtime` is emitted JS without sibling `.d.ts` in this repo layout.
|
|
23
|
+
// @ts-expect-error TS7016 — intentional dynamic import of artifact bundle
|
|
24
|
+
const cliIndex = (await import("./runtime/cli/index.js"));
|
|
25
|
+
const commandsMod = (await import("./runtime/cli/commands/index.js"));
|
|
26
|
+
const storageDb = (await import("./runtime/storage/db/index.js"));
|
|
27
|
+
const obsDb = (await import("./runtime/observability/db/index.js"));
|
|
28
|
+
const boundary = (await import("./runtime/cli/runtime/runtime-artifact-boundary.js"));
|
|
29
|
+
const dataDir = path.join(resolvedRoot, "data");
|
|
30
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
31
|
+
const statePath = path.join(dataDir, "state.db");
|
|
32
|
+
const obsPath = path.join(dataDir, "observability.db");
|
|
33
|
+
const stateDb = storageDb.createStateDatabase(statePath);
|
|
34
|
+
const observabilityDb = obsDb.createObservabilityDatabase(obsPath);
|
|
35
|
+
const deps = cliIndex.createCliRuntimeDeps({ stateDb, observabilityDb });
|
|
36
|
+
const runtimeResolved = boundary.resolvePackagedRuntime(PLUGIN_PACKAGE_ROOT);
|
|
37
|
+
const opsRouter = cliIndex.createOpsRouter({
|
|
38
|
+
runtimeAvailable: runtimeResolved.ok,
|
|
39
|
+
readModels: deps.readModels,
|
|
40
|
+
});
|
|
41
|
+
const commands = commandsMod.createCliCommands({
|
|
42
|
+
readModels: deps.readModels,
|
|
43
|
+
actionBridge: deps.actionBridge,
|
|
44
|
+
opsRouter,
|
|
45
|
+
});
|
|
46
|
+
const dispatch = async (command, input) => {
|
|
47
|
+
const def = commands.find((c) => c.name === command);
|
|
48
|
+
if (!def) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: { code: "unknown_command", message: `Unknown Second Nature command: ${command}` },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const prevCwd = process.cwd();
|
|
55
|
+
try {
|
|
56
|
+
process.chdir(resolvedRoot);
|
|
57
|
+
return await def.execute(input);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
process.chdir(prevCwd);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const close = () => {
|
|
64
|
+
cliIndex.closeCliRuntimeDeps(deps);
|
|
65
|
+
};
|
|
66
|
+
return { ok: true, workspaceRoot: resolvedRoot, dispatch, close };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
error: {
|
|
72
|
+
code: "WORKSPACE_FULL_OPS_BRIDGE_FAILED",
|
|
73
|
+
message: error instanceof Error ? error.message : String(error),
|
|
74
|
+
nextStep: "Confirm the packaged plugin includes plugin/runtime and that the host allows dynamic import of sql.js. If the VM forbids it, use a subprocess workspace CLI (Plan B) or run outside the sandbox.",
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|