@kingsoftcloud/agentkit-harness-node 0.0.0-stage → 0.0.2
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/LICENSE +201 -0
- package/NOTICE +1 -0
- package/README.md +124 -2
- package/THIRD_PARTY_NOTICES.md +66 -0
- package/ZCode-LICENSE +202 -0
- package/dist/index.mjs +4871 -0
- package/dist/native-command-plan.LICENSE.txt +221 -0
- package/dist/native-command-plan.json +16 -0
- package/dist/native-command-plan.mjs +8 -0
- package/dist/rpc-entry.mjs +5682 -0
- package/package.json +169 -4
- package/src/native/a2a-receipt-source.mjs +19 -0
- package/src/native/a2a-tools.mjs +517 -0
- package/src/native/activity-status.mjs +30 -0
- package/src/native/artifact-codec.mjs +481 -0
- package/src/native/artifact-store.mjs +1162 -0
- package/src/native/assistant-stream.mjs +76 -0
- package/src/native/background-command-port.mjs +95 -0
- package/src/native/browser-action-observation.mjs +43 -0
- package/src/native/browser-availability.mjs +22 -0
- package/src/native/browser-canvas-metrics.mjs +71 -0
- package/src/native/browser-dom.mjs +193 -0
- package/src/native/browser-playwright.mjs +1249 -0
- package/src/native/browser-vision.mjs +369 -0
- package/src/native/browser-workspace-policy.mjs +22 -0
- package/src/native/browser-workspace-protocol.mjs +95 -0
- package/src/native/browser-workspace-server.mjs +164 -0
- package/src/native/browser-workspace.mjs +355 -0
- package/src/native/captured-source.mjs +3 -0
- package/src/native/child-budget-allocation.mjs +87 -0
- package/src/native/child-capabilities.mjs +35 -0
- package/src/native/child-command-resource.mjs +20 -0
- package/src/native/child-history.mjs +188 -0
- package/src/native/child-merge.mjs +98 -0
- package/src/native/child-model-surface.mjs +19 -0
- package/src/native/child-observation.mjs +100 -0
- package/src/native/child-owner.mjs +620 -0
- package/src/native/child-publication.mjs +76 -0
- package/src/native/child-readers.mjs +366 -0
- package/src/native/child-tools.mjs +46 -0
- package/src/native/child-workspace-binding.mjs +29 -0
- package/src/native/child-workspace-tools.mjs +1 -0
- package/src/native/child-workspaces.mjs +655 -0
- package/src/native/children.mjs +365 -0
- package/src/native/command-plan-worker.mjs +14 -0
- package/src/native/command-plan.mjs +31 -0
- package/src/native/command-progress.mjs +64 -0
- package/src/native/command-shell.mjs +9 -0
- package/src/native/command-workspace-observation.mjs +36 -0
- package/src/native/component-tools.mjs +280 -0
- package/src/native/context-counter.mjs +133 -0
- package/src/native/context-markers.mjs +51 -0
- package/src/native/context-profile.mjs +72 -0
- package/src/native/context-status.mjs +27 -0
- package/src/native/conversation-command-control.mjs +133 -0
- package/src/native/delivery-timeline.mjs +39 -0
- package/src/native/dsh-background-ownership.mjs +102 -0
- package/src/native/dsh-managed-ownership.mjs +23 -0
- package/src/native/dsh-turn-contract.mjs +3 -0
- package/src/native/dsh-turn-ownership.mjs +54 -0
- package/src/native/dsh-turn-runtime.mjs +498 -0
- package/src/native/e2b-background-supervisor.mjs +347 -0
- package/src/native/e2b-background.mjs +368 -0
- package/src/native/e2b-command-state.mjs +40 -0
- package/src/native/e2b-command-tools.mjs +486 -0
- package/src/native/e2b-sandbox-owner.mjs +227 -0
- package/src/native/hook-executor.mjs +249 -0
- package/src/native/instruction-assembly.mjs +60 -0
- package/src/native/instruction-context.mjs +54 -0
- package/src/native/local-background-supervisor.mjs +108 -0
- package/src/native/local-background.mjs +207 -0
- package/src/native/local-command-evidence.mjs +57 -0
- package/src/native/local-command-policy-worker.mjs +197 -0
- package/src/native/local-command-policy.mjs +46 -0
- package/src/native/local-command-state.mjs +177 -0
- package/src/native/local-command-tools.mjs +302 -0
- package/src/native/local-execution-profile.mjs +36 -0
- package/src/native/local-output-policy.mjs +92 -0
- package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
- package/src/native/local-process-bin/darwin-x64/launcher +0 -0
- package/src/native/local-process-bin/linux-arm64/launcher +0 -0
- package/src/native/local-process-bin/linux-x64/launcher +0 -0
- package/src/native/local-process-launcher.c +183 -0
- package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
- package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
- package/src/native/local-process-licenses/NOTICE +21 -0
- package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
- package/src/native/local-process-manifest.json +32 -0
- package/src/native/local-process.mjs +246 -0
- package/src/native/local-resource-profile.mjs +32 -0
- package/src/native/memory-candidate.mjs +121 -0
- package/src/native/memory-extraction-run.mjs +60 -0
- package/src/native/memory-extraction-tools.mjs +116 -0
- package/src/native/memory-recall.mjs +65 -0
- package/src/native/memory-store.mjs +334 -0
- package/src/native/memory-tools.mjs +329 -0
- package/src/native/model-anthropic-cache.mjs +49 -0
- package/src/native/model-anthropic-tool-search.mjs +85 -0
- package/src/native/model-anthropic.mjs +753 -0
- package/src/native/model-chat-usage.mjs +24 -0
- package/src/native/model-inline-reasoning.mjs +111 -0
- package/src/native/model-media.mjs +174 -0
- package/src/native/model-normalize.mjs +228 -0
- package/src/native/model-openai-chat.mjs +561 -0
- package/src/native/model-port.mjs +361 -0
- package/src/native/model-provider-compat.mjs +129 -0
- package/src/native/model-replay.mjs +153 -0
- package/src/native/model-request.mjs +15 -0
- package/src/native/model-responses.mjs +1205 -0
- package/src/native/model-sse.mjs +171 -0
- package/src/native/model-surface-host.mjs +27 -0
- package/src/native/model-tool-search-policy.mjs +16 -0
- package/src/native/model-transport-observation.mjs +27 -0
- package/src/native/owned-child.mjs +114 -0
- package/src/native/owned-command.mjs +106 -0
- package/src/native/python-syntax-worker.mjs +60 -0
- package/src/native/python-syntax.mjs +50 -0
- package/src/native/read-interruption.mjs +16 -0
- package/src/native/result-stream.mjs +63 -0
- package/src/native/ruff-ast.mjs +95 -0
- package/src/native/run-observation.mjs +128 -0
- package/src/native/session-delivery-journal.mjs +96 -0
- package/src/native/session-delivery.mjs +434 -0
- package/src/native/shell-analysis.mjs +50 -0
- package/src/native/skill-disclosure.mjs +404 -0
- package/src/native/skill-events.mjs +164 -0
- package/src/native/skill-tools.mjs +813 -0
- package/src/native/source-page.mjs +73 -0
- package/src/native/sqlite-session-inbox.mjs +42 -0
- package/src/native/sqlite-store.mjs +617 -0
- package/src/native/tool-authority.mjs +99 -0
- package/src/native/tool-guidance.mjs +12 -0
- package/src/native/tool-observation.mjs +75 -0
- package/src/native/tool-states.mjs +14 -0
- package/src/native/verification-authority.mjs +37 -0
- package/src/native/web-tools.mjs +643 -0
- package/src/native/workspace-checkpoints.mjs +312 -0
- package/src/native/workspace-context.mjs +61 -0
- package/src/native/workspace-display.mjs +39 -0
- package/src/native/workspace-edit-match.mjs +19 -0
- package/src/native/workspace-edit-worker.mjs +15 -0
- package/src/native/workspace-editing.mjs +447 -0
- package/src/native/workspace-image.mjs +72 -0
- package/src/native/workspace-instructions.mjs +107 -0
- package/src/native/workspace-mutation-lock.mjs +74 -0
- package/src/native/workspace-patch-worker.mjs +17 -0
- package/src/native/workspace-patch.mjs +17 -0
- package/src/native/workspace-paths.mjs +225 -0
- package/src/native/workspace-state.mjs +272 -0
- package/src/native/workspace-subject-observer.mjs +84 -0
- package/src/native/workspace-tools.mjs +1740 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The Node local-process adapter uses a small executable built from the adjacent
|
|
2
|
+
local-process-launcher.c (the repository's Apache-2.0 license). It performs only
|
|
3
|
+
POSIX resource-limit, fork/watchdog and exec operations; it is not a language
|
|
4
|
+
interpreter or an execution policy.
|
|
5
|
+
|
|
6
|
+
Darwin executables dynamically use the operating system's libSystem; no Apple
|
|
7
|
+
runtime library is bundled. Linux executables are built with Alpine GCC 14.2.0
|
|
8
|
+
and statically link musl 1.2.5. The Linux owner watchdog enforces
|
|
9
|
+
maxProcesses per process group because RLIMIT_NPROC is scoped to the whole real
|
|
10
|
+
user on shared hosts. The included musl COPYRIGHT is from the upstream 1.2.5
|
|
11
|
+
source release. GCC runtime terms are included for compiler runtime parts:
|
|
12
|
+
COPYING3 plus the GCC Runtime Library Exception (COPYING.RUNTIME).
|
|
13
|
+
|
|
14
|
+
Upstream license sources:
|
|
15
|
+
https://musl.libc.org/releases/musl-1.2.5.tar.gz (COPYRIGHT)
|
|
16
|
+
https://github.com/gcc-mirror/gcc/blob/releases/gcc-14.2.0/COPYING3
|
|
17
|
+
https://github.com/gcc-mirror/gcc/blob/releases/gcc-14.2.0/COPYING.RUNTIME
|
|
18
|
+
|
|
19
|
+
Compiler identification, base-image digest and each executable's SHA-256 are
|
|
20
|
+
recorded in local-process-manifest.json. Rebuilds are a maintainer operation;
|
|
21
|
+
installation and execution never invoke a compiler, package manager or Python.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
musl as a whole is licensed under the following standard MIT license:
|
|
2
|
+
|
|
3
|
+
----------------------------------------------------------------------
|
|
4
|
+
Copyright © 2005-2020 Rich Felker, et al.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
7
|
+
a copy of this software and associated documentation files (the
|
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
12
|
+
the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be
|
|
15
|
+
included in all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
20
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
21
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
22
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
23
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
24
|
+
----------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
Authors/contributors include:
|
|
27
|
+
|
|
28
|
+
A. Wilcox
|
|
29
|
+
Ada Worcester
|
|
30
|
+
Alex Dowad
|
|
31
|
+
Alex Suykov
|
|
32
|
+
Alexander Monakov
|
|
33
|
+
Andre McCurdy
|
|
34
|
+
Andrew Kelley
|
|
35
|
+
Anthony G. Basile
|
|
36
|
+
Aric Belsito
|
|
37
|
+
Arvid Picciani
|
|
38
|
+
Bartosz Brachaczek
|
|
39
|
+
Benjamin Peterson
|
|
40
|
+
Bobby Bingham
|
|
41
|
+
Boris Brezillon
|
|
42
|
+
Brent Cook
|
|
43
|
+
Chris Spiegel
|
|
44
|
+
Clément Vasseur
|
|
45
|
+
Daniel Micay
|
|
46
|
+
Daniel Sabogal
|
|
47
|
+
Daurnimator
|
|
48
|
+
David Carlier
|
|
49
|
+
David Edelsohn
|
|
50
|
+
Denys Vlasenko
|
|
51
|
+
Dmitry Ivanov
|
|
52
|
+
Dmitry V. Levin
|
|
53
|
+
Drew DeVault
|
|
54
|
+
Emil Renner Berthing
|
|
55
|
+
Fangrui Song
|
|
56
|
+
Felix Fietkau
|
|
57
|
+
Felix Janda
|
|
58
|
+
Gianluca Anzolin
|
|
59
|
+
Hauke Mehrtens
|
|
60
|
+
He X
|
|
61
|
+
Hiltjo Posthuma
|
|
62
|
+
Isaac Dunham
|
|
63
|
+
Jaydeep Patil
|
|
64
|
+
Jens Gustedt
|
|
65
|
+
Jeremy Huntwork
|
|
66
|
+
Jo-Philipp Wich
|
|
67
|
+
Joakim Sindholt
|
|
68
|
+
John Spencer
|
|
69
|
+
Julien Ramseier
|
|
70
|
+
Justin Cormack
|
|
71
|
+
Kaarle Ritvanen
|
|
72
|
+
Khem Raj
|
|
73
|
+
Kylie McClain
|
|
74
|
+
Leah Neukirchen
|
|
75
|
+
Luca Barbato
|
|
76
|
+
Luka Perkov
|
|
77
|
+
M Farkas-Dyck (Strake)
|
|
78
|
+
Mahesh Bodapati
|
|
79
|
+
Markus Wichmann
|
|
80
|
+
Masanori Ogino
|
|
81
|
+
Michael Clark
|
|
82
|
+
Michael Forney
|
|
83
|
+
Mikhail Kremnyov
|
|
84
|
+
Natanael Copa
|
|
85
|
+
Nicholas J. Kain
|
|
86
|
+
orc
|
|
87
|
+
Pascal Cuoq
|
|
88
|
+
Patrick Oppenlander
|
|
89
|
+
Petr Hosek
|
|
90
|
+
Petr Skocik
|
|
91
|
+
Pierre Carrier
|
|
92
|
+
Reini Urban
|
|
93
|
+
Rich Felker
|
|
94
|
+
Richard Pennington
|
|
95
|
+
Ryan Fairfax
|
|
96
|
+
Samuel Holland
|
|
97
|
+
Segev Finer
|
|
98
|
+
Shiz
|
|
99
|
+
sin
|
|
100
|
+
Solar Designer
|
|
101
|
+
Stefan Kristiansson
|
|
102
|
+
Stefan O'Rear
|
|
103
|
+
Szabolcs Nagy
|
|
104
|
+
Timo Teräs
|
|
105
|
+
Trutz Behn
|
|
106
|
+
Valentin Ochs
|
|
107
|
+
Will Dietz
|
|
108
|
+
William Haddon
|
|
109
|
+
William Pitcock
|
|
110
|
+
|
|
111
|
+
Portions of this software are derived from third-party works licensed
|
|
112
|
+
under terms compatible with the above MIT license:
|
|
113
|
+
|
|
114
|
+
The TRE regular expression implementation (src/regex/reg* and
|
|
115
|
+
src/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed
|
|
116
|
+
under a 2-clause BSD license (license text in the source files). The
|
|
117
|
+
included version has been heavily modified by Rich Felker in 2012, in
|
|
118
|
+
the interests of size, simplicity, and namespace cleanliness.
|
|
119
|
+
|
|
120
|
+
Much of the math library code (src/math/* and src/complex/*) is
|
|
121
|
+
Copyright © 1993,2004 Sun Microsystems or
|
|
122
|
+
Copyright © 2003-2011 David Schultz or
|
|
123
|
+
Copyright © 2003-2009 Steven G. Kargl or
|
|
124
|
+
Copyright © 2003-2009 Bruce D. Evans or
|
|
125
|
+
Copyright © 2008 Stephen L. Moshier or
|
|
126
|
+
Copyright © 2017-2018 Arm Limited
|
|
127
|
+
and labelled as such in comments in the individual source files. All
|
|
128
|
+
have been licensed under extremely permissive terms.
|
|
129
|
+
|
|
130
|
+
The ARM memcpy code (src/string/arm/memcpy.S) is Copyright © 2008
|
|
131
|
+
The Android Open Source Project and is licensed under a two-clause BSD
|
|
132
|
+
license. It was taken from Bionic libc, used on Android.
|
|
133
|
+
|
|
134
|
+
The AArch64 memcpy and memset code (src/string/aarch64/*) are
|
|
135
|
+
Copyright © 1999-2019, Arm Limited.
|
|
136
|
+
|
|
137
|
+
The implementation of DES for crypt (src/crypt/crypt_des.c) is
|
|
138
|
+
Copyright © 1994 David Burren. It is licensed under a BSD license.
|
|
139
|
+
|
|
140
|
+
The implementation of blowfish crypt (src/crypt/crypt_blowfish.c) was
|
|
141
|
+
originally written by Solar Designer and placed into the public
|
|
142
|
+
domain. The code also comes with a fallback permissive license for use
|
|
143
|
+
in jurisdictions that may not recognize the public domain.
|
|
144
|
+
|
|
145
|
+
The smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011
|
|
146
|
+
Valentin Ochs and is licensed under an MIT-style license.
|
|
147
|
+
|
|
148
|
+
The x86_64 port was written by Nicholas J. Kain and is licensed under
|
|
149
|
+
the standard MIT terms.
|
|
150
|
+
|
|
151
|
+
The mips and microblaze ports were originally written by Richard
|
|
152
|
+
Pennington for use in the ellcc project. The original code was adapted
|
|
153
|
+
by Rich Felker for build system and code conventions during upstream
|
|
154
|
+
integration. It is licensed under the standard MIT terms.
|
|
155
|
+
|
|
156
|
+
The mips64 port was contributed by Imagination Technologies and is
|
|
157
|
+
licensed under the standard MIT terms.
|
|
158
|
+
|
|
159
|
+
The powerpc port was also originally written by Richard Pennington,
|
|
160
|
+
and later supplemented and integrated by John Spencer. It is licensed
|
|
161
|
+
under the standard MIT terms.
|
|
162
|
+
|
|
163
|
+
All other files which have no copyright comments are original works
|
|
164
|
+
produced specifically for use as part of this library, written either
|
|
165
|
+
by Rich Felker, the main author of the library, or by one or more
|
|
166
|
+
contibutors listed above. Details on authorship of individual files
|
|
167
|
+
can be found in the git version control history of the project. The
|
|
168
|
+
omission of copyright and license comments in each file is in the
|
|
169
|
+
interest of source tree size.
|
|
170
|
+
|
|
171
|
+
In addition, permission is hereby granted for all public header files
|
|
172
|
+
(include/* and arch/*/bits/*) and crt files intended to be linked into
|
|
173
|
+
applications (crt/*, ldso/dlstart.c, and arch/*/crt_arch.h) to omit
|
|
174
|
+
the copyright notice and permission notice otherwise required by the
|
|
175
|
+
license, and to use these files without any requirement of
|
|
176
|
+
attribution. These files include substantial contributions from:
|
|
177
|
+
|
|
178
|
+
Bobby Bingham
|
|
179
|
+
John Spencer
|
|
180
|
+
Nicholas J. Kain
|
|
181
|
+
Rich Felker
|
|
182
|
+
Richard Pennington
|
|
183
|
+
Stefan Kristiansson
|
|
184
|
+
Szabolcs Nagy
|
|
185
|
+
|
|
186
|
+
all of whom have explicitly granted such permission.
|
|
187
|
+
|
|
188
|
+
This file previously contained text expressing a belief that most of
|
|
189
|
+
the files covered by the above exception were sufficiently trivial not
|
|
190
|
+
to be subject to copyright, resulting in confusion over whether it
|
|
191
|
+
negated the permissions granted in the license. In the spirit of
|
|
192
|
+
permissive licensing, and of not having licensing issues being an
|
|
193
|
+
obstacle to adoption, that text has been removed.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "agentkit.local-process-binary/v1",
|
|
3
|
+
"sourceSha256": "aec24ff7327319074bd7dd66fcde657f55179556f8a7e954303ea3f9eb135f0e",
|
|
4
|
+
"builds": {
|
|
5
|
+
"darwin-arm64": {
|
|
6
|
+
"file": "local-process-bin/darwin-arm64/launcher",
|
|
7
|
+
"sha256": "202cb67e15c09285123b60a9b72c4a8b5824e49e3d8c30746eae2ffb681c22e8",
|
|
8
|
+
"bytes": 50944,
|
|
9
|
+
"compiler": "Apple clang version 21.0.0 (clang-2100.3.34.2)"
|
|
10
|
+
},
|
|
11
|
+
"darwin-x64": {
|
|
12
|
+
"file": "local-process-bin/darwin-x64/launcher",
|
|
13
|
+
"sha256": "756085384f20c8690b43ea4b304b5acd10ecb438f12c51cf1d78ad545d594fe3",
|
|
14
|
+
"bytes": 17640,
|
|
15
|
+
"compiler": "Apple clang version 21.0.0 (clang-2100.3.34.2)"
|
|
16
|
+
},
|
|
17
|
+
"linux-x64": {
|
|
18
|
+
"file": "local-process-bin/linux-x64/launcher",
|
|
19
|
+
"sha256": "15205c3219e9771855c0c64812639a589f5d58d2cb6cb5ede9c07a416ecb64b1",
|
|
20
|
+
"bytes": 62840,
|
|
21
|
+
"compiler": "cc (Alpine 14.2.0) 14.2.0",
|
|
22
|
+
"image": "docker.m.daocloud.io/library/alpine@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412"
|
|
23
|
+
},
|
|
24
|
+
"linux-arm64": {
|
|
25
|
+
"file": "local-process-bin/linux-arm64/launcher",
|
|
26
|
+
"sha256": "c59bf87805d0887cd142f04eff5047146f5ec5d594d400042bda3ce40984b670",
|
|
27
|
+
"bytes": 132376,
|
|
28
|
+
"compiler": "cc (Alpine 14.2.0) 14.2.0",
|
|
29
|
+
"image": "docker.m.daocloud.io/library/alpine@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Node owns the bounded lifetime; a packaged, verified POSIX exec substrate
|
|
2
|
+
// applies resource controls and an owner-pipe watchdog. This is a risk control for
|
|
3
|
+
// same-container commands, not an OS security sandbox or an execution policy.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { readFileSync, lstatSync, constants } from 'node:fs';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { constants as osConstants } from 'node:os';
|
|
10
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
11
|
+
import { performance } from 'node:perf_hooks';
|
|
12
|
+
import { localEnvironmentPolicy, localOutputPolicy, resolveEnvironmentPolicy, resolveOutputPolicy } from './local-output-policy.mjs';
|
|
13
|
+
|
|
14
|
+
export const LOCAL_PROCESS_CONTRACT = 'agentkit.local-process-control/v1';
|
|
15
|
+
const directory = fileURLToPath(new URL('./', import.meta.url));
|
|
16
|
+
const defaults = Object.freeze({ cpuSeconds: 120, addressSpaceBytes: 1024 ** 3, maxProcesses: 64,
|
|
17
|
+
maxOpenFiles: 256, maxFileBytes: 64 * 1024 ** 2, maxOutputBytes: 1024 ** 2, wallSeconds: 900, extraEnvNames: [] });
|
|
18
|
+
const limitNames = ['cpuSeconds', 'addressSpaceBytes', 'maxProcesses', 'maxOpenFiles', 'maxFileBytes'];
|
|
19
|
+
export class LocalProcessError extends Error { constructor(code) { super(code); this.name = 'LocalProcessError'; this.code = code; } }
|
|
20
|
+
const check = (value, code) => { if (!value) throw new LocalProcessError(code); };
|
|
21
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
22
|
+
const wait = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
23
|
+
async function boundedWait(promise, milliseconds) {
|
|
24
|
+
let timer;
|
|
25
|
+
try { await Promise.race([promise, new Promise(resolve => { timer = setTimeout(resolve, milliseconds); })]); }
|
|
26
|
+
finally { clearTimeout(timer); }
|
|
27
|
+
}
|
|
28
|
+
const namePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
29
|
+
export function localControlSettings(input = {}) {
|
|
30
|
+
check(input && typeof input === 'object' && !Array.isArray(input)
|
|
31
|
+
&& Object.keys(input).every(key => Object.hasOwn(defaults, key)), 'invalid_local_process_settings');
|
|
32
|
+
const settings = { ...defaults, ...input };
|
|
33
|
+
for (const [name, value] of Object.entries(settings)) if (name !== 'extraEnvNames')
|
|
34
|
+
check(Number.isSafeInteger(value) && value > 0, `invalid_local_process_limit:${name}`);
|
|
35
|
+
check(settings.wallSeconds <= Number.MAX_SAFE_INTEGER / 1000, 'invalid_local_process_wall_limit');
|
|
36
|
+
check(Array.isArray(settings.extraEnvNames) && settings.extraEnvNames.every(name => typeof name === 'string'
|
|
37
|
+
&& namePattern.test(name) && !name.startsWith('KSADK_LOCAL_PROCESS_')), 'invalid_local_process_environment_allowlist');
|
|
38
|
+
settings.extraEnvNames = Object.freeze([...new Set(settings.extraEnvNames)]);
|
|
39
|
+
return Object.freeze(settings);
|
|
40
|
+
}
|
|
41
|
+
export function buildScriptEnvironment(source, { settings = localControlSettings(), required = {}, environmentPolicy = null } = {}) {
|
|
42
|
+
const policy = localControlSettings(settings), env = resolveEnvironmentPolicy(policy, localEnvironmentPolicy(environmentPolicy)), result = Object.create(null);
|
|
43
|
+
for (const name of ['PATH', 'LANG', 'LC_ALL', 'TZ', 'SYSTEMROOT', ...env.names, ...policy.extraEnvNames])
|
|
44
|
+
if (Object.hasOwn(source, name)) result[name] = String(source[name]);
|
|
45
|
+
result.PATH ??= '/bin:/usr/bin'; result.LANG ??= 'C.UTF-8'; result.CI = '1';
|
|
46
|
+
for (const [name, value] of Object.entries(required)) { check(namePattern.test(name), 'invalid_local_process_environment'); result[name] = String(value); }
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
export function supportsLocalProcessPlatform({ platform = process.platform, arch = process.arch } = {}) {
|
|
50
|
+
return ['darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64'].includes(`${platform}-${arch}`);
|
|
51
|
+
}
|
|
52
|
+
export function resolveLocalProcessLauncher({ platform = process.platform, arch = process.arch } = {}) {
|
|
53
|
+
const target = `${platform}-${arch}`;
|
|
54
|
+
check(supportsLocalProcessPlatform({ platform, arch }), 'unsupported_local_process_platform');
|
|
55
|
+
const manifest = JSON.parse(readFileSync(join(directory, 'local-process-manifest.json'), 'utf8'));
|
|
56
|
+
check(manifest.schemaVersion === 'agentkit.local-process-binary/v1'
|
|
57
|
+
&& manifest.sourceSha256 === hash(readFileSync(join(directory, 'local-process-launcher.c'))), 'local_process_source_digest_mismatch');
|
|
58
|
+
const entry = manifest.builds?.[target], relative = `local-process-bin/${target}/launcher`;
|
|
59
|
+
check(entry?.file === relative, 'local_process_launcher_missing');
|
|
60
|
+
const filename = join(directory, relative), stat = lstatSync(filename);
|
|
61
|
+
check(stat.isFile() && (stat.mode & 0o111) !== 0, 'local_process_launcher_not_executable');
|
|
62
|
+
const bytes = readFileSync(filename, { flag: constants.O_RDONLY | constants.O_NOFOLLOW });
|
|
63
|
+
check(bytes.length === entry.bytes && hash(bytes) === entry.sha256, 'local_process_launcher_digest_mismatch');
|
|
64
|
+
return filename;
|
|
65
|
+
}
|
|
66
|
+
function controlReport(text, settings) {
|
|
67
|
+
try {
|
|
68
|
+
const report = JSON.parse(text);
|
|
69
|
+
check(report.schemaVersion === LOCAL_PROCESS_CONTRACT && ['applied', 'partial', 'failed'].includes(report.status)
|
|
70
|
+
&& report.applied && typeof report.applied === 'object' && !Array.isArray(report.applied)
|
|
71
|
+
&& Array.isArray(report.unsupported) && Array.isArray(report.failed), 'invalid_local_process_control_report');
|
|
72
|
+
check(Object.entries(report.applied).every(([key, value]) => limitNames.includes(key)
|
|
73
|
+
&& Number.isSafeInteger(value) && value >= 0 && value <= settings[key]), 'invalid_local_process_control_report');
|
|
74
|
+
check(report.unsupported.every(key => limitNames.includes(key)) && report.failed.every(key => [...limitNames, 'owner_watchdog'].includes(key)),
|
|
75
|
+
'invalid_local_process_control_report');
|
|
76
|
+
if (report.status !== 'failed') {
|
|
77
|
+
const allowedUnsupported = process.platform === 'darwin' ? ['addressSpaceBytes', 'maxProcesses'] : [];
|
|
78
|
+
check(Number.isSafeInteger(report.watcherPid) && report.watcherPid > 0 && !report.failed.length
|
|
79
|
+
&& report.unsupported.length === allowedUnsupported.length && allowedUnsupported.every(key => report.unsupported.includes(key))
|
|
80
|
+
&& limitNames.every(key => Object.hasOwn(report.applied, key) !== report.unsupported.includes(key))
|
|
81
|
+
&& report.status === (allowedUnsupported.length ? 'partial' : 'applied'), 'invalid_local_process_control_report');
|
|
82
|
+
}
|
|
83
|
+
const { watcherPid, ...publicReport } = report; return publicReport;
|
|
84
|
+
} catch { return { schemaVersion: LOCAL_PROCESS_CONTRACT, status: 'failed', applied: {}, unsupported: [], failed: ['control_report'] }; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** argv/env are already Host-authorized. Shell/AST/workspace policy belongs to
|
|
88
|
+
* the caller; no retries and no durable handle ownership is inferred here.
|
|
89
|
+
* outputPolicy is opt-in (persisted-stream/v1); when absent, legacy
|
|
90
|
+
* maxOutputBytes-then-kill semantics and result shape are preserved exactly. */
|
|
91
|
+
export async function runBoundedProcess({ argv, cwd, env, settings: input = {}, timeoutMs = null, signal, onOutput, onSpawn, outputPolicy = null } = {}) {
|
|
92
|
+
const settings = localControlSettings(input); signal?.throwIfAborted();
|
|
93
|
+
check(Array.isArray(argv) && argv.length > 0 && argv.every(value => typeof value === 'string' && !value.includes('\0'))
|
|
94
|
+
&& argv[0].length > 0 && typeof cwd === 'string' && cwd.length > 0, 'invalid_local_process_command');
|
|
95
|
+
check(env && typeof env === 'object' && !Array.isArray(env) && Object.entries(env).every(([name, value]) =>
|
|
96
|
+
namePattern.test(name) && typeof value === 'string' && !value.includes('\0')), 'invalid_local_process_environment');
|
|
97
|
+
check(timeoutMs === null || Number.isFinite(timeoutMs) && timeoutMs > 0, 'invalid_local_process_timeout');
|
|
98
|
+
const outPolicy = resolveOutputPolicy(settings, localOutputPolicy(outputPolicy));
|
|
99
|
+
const executable = resolveLocalProcessLauncher();
|
|
100
|
+
const started = performance.now(), duration = Math.min(timeoutMs ?? Infinity, settings.wallSeconds * 1000);
|
|
101
|
+
const child = spawn(executable, [...limitNames.map(key => String(settings[key])), '3', '4', '--', ...argv], {
|
|
102
|
+
cwd, env, detached: true, stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
103
|
+
});
|
|
104
|
+
const output = { stdout: [], stderr: [] }, lengths = { stdout: 0, stderr: 0 };
|
|
105
|
+
const truncated = { stdout: false, stderr: false }, decoders = { stdout: new StringDecoder('utf8'), stderr: new StringDecoder('utf8') };
|
|
106
|
+
const report = []; let reportBytes = 0, timedOut = false, canceled = false, outputLimitExceeded = false;
|
|
107
|
+
const cleanupErrors = new Set(); let exitCode = null, exitSignal = null, exited = false, closed = false, spawnError = null, timer;
|
|
108
|
+
let stop; const stopping = new Promise(resolve => { stop = resolve; });
|
|
109
|
+
let exitDone; const exit = new Promise(resolve => { exitDone = resolve; });
|
|
110
|
+
let closeDone; const close = new Promise(resolve => { closeDone = resolve; });
|
|
111
|
+
child.once('error', error => { spawnError = error; exitDone(); stop(); });
|
|
112
|
+
child.once('exit', (code, signal) => { exited = true; exitCode = code; exitSignal = signal; exitDone(); stop(); });
|
|
113
|
+
child.once('close', () => { closed = true; closeDone(); });
|
|
114
|
+
child.once('spawn', () => { try { onSpawn?.({ pid: child.pid }); } catch { cleanupErrors.add('spawn_observer_failed'); stop(); } });
|
|
115
|
+
// persisted-stream/v1: the full capped UTF-8 output is streamed out via onOutput
|
|
116
|
+
// to the existing supervisor-owned files; result.stdout/stderr hold ONLY a bounded
|
|
117
|
+
// preview. No full 64MiB buffer is held in memory here. The supervisor fsyncs the
|
|
118
|
+
// streamed files before its terminal receipt and never overwrites them with the
|
|
119
|
+
// preview. Only the hard persisted cap, timeout, cancellation, or real resource
|
|
120
|
+
// limits stop the process; preview overflow never kills a successful process.
|
|
121
|
+
const persistedStream = outPolicy.mode === 'persistedStream';
|
|
122
|
+
const preview = persistedStream ? { stdout: [], stderr: [], lengths: { stdout: 0, stderr: 0 },
|
|
123
|
+
truncated: { stdout: false, stderr: false } } : null;
|
|
124
|
+
const stream = persistedStream ? { lengths: { stdout: 0, stderr: 0 }, emitted: { stdout: 0, stderr: 0 }, truncated: { stdout: false, stderr: false },
|
|
125
|
+
decoders: { stdout: new StringDecoder('utf8'), stderr: new StringDecoder('utf8') } } : null;
|
|
126
|
+
const utf8Prefix = (text, limit) => new StringDecoder('utf8').write(Buffer.from(text).subarray(0, limit));
|
|
127
|
+
const emitPersisted = (channel, text) => {
|
|
128
|
+
// Invalid source bytes may expand into replacement characters. Bound the
|
|
129
|
+
// encoded observation too, so normalization cannot exceed the persisted cap.
|
|
130
|
+
const remaining = outPolicy.persistedBytes - stream.emitted[channel];
|
|
131
|
+
const bounded = utf8Prefix(text, remaining);
|
|
132
|
+
stream.emitted[channel] += Buffer.byteLength(bounded);
|
|
133
|
+
if (bounded) { try { onOutput?.(channel, bounded); } catch { /* supervisor records its own write failure */ } }
|
|
134
|
+
if (bounded !== text) { stream.truncated[channel] = true; outputLimitExceeded = true; stop(); }
|
|
135
|
+
};
|
|
136
|
+
for (const channel of ['stdout', 'stderr']) {
|
|
137
|
+
child[channel].on('error', () => { cleanupErrors.add(`${channel}_read_error`); stop(); });
|
|
138
|
+
child[channel].on('data', chunk => {
|
|
139
|
+
if (!persistedStream) {
|
|
140
|
+
const remaining = settings.maxOutputBytes - lengths[channel], kept = chunk.subarray(0, Math.max(0, remaining));
|
|
141
|
+
if (kept.length) {
|
|
142
|
+
output[channel].push(kept); lengths[channel] += kept.length;
|
|
143
|
+
const text = decoders[channel].write(kept);
|
|
144
|
+
if (text) { try { onOutput?.(channel, text); } catch { /* observation never changes execution */ } }
|
|
145
|
+
}
|
|
146
|
+
if (chunk.length > remaining) { truncated[channel] = true; outputLimitExceeded = true; stop(); }
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Hard persisted cap: stream all bytes up to this bound via onOutput (to the
|
|
150
|
+
// owned files). Beyond it the process is stopped — this is the real boundary.
|
|
151
|
+
const pRemaining = outPolicy.persistedBytes - stream.lengths[channel];
|
|
152
|
+
const pKept = chunk.subarray(0, Math.max(0, pRemaining));
|
|
153
|
+
const overcap = chunk.length > pRemaining;
|
|
154
|
+
if (pKept.length) {
|
|
155
|
+
stream.lengths[channel] += pKept.length;
|
|
156
|
+
// Stream complete characters only; the StringDecoder holds a trailing
|
|
157
|
+
// partial multibyte sequence rather than emitting it. We never flush that
|
|
158
|
+
// held tail as replacement bytes (end()), so the file holds at most
|
|
159
|
+
// persistedBytes of valid UTF-8 — a cap-cut multibyte sequence is dropped,
|
|
160
|
+
// not written as a 3-byte replacement char beyond the cap.
|
|
161
|
+
const text = stream.decoders[channel].write(pKept);
|
|
162
|
+
if (text) emitPersisted(channel, text);
|
|
163
|
+
}
|
|
164
|
+
// Bounded in-memory preview: keep only up to previewBytes for the receipt,
|
|
165
|
+
// captured from pKept BEFORE the hard-cap return so an overcap first chunk
|
|
166
|
+
// still yields a non-empty preview. Preview overflow is honest (truncated
|
|
167
|
+
// flag) but never stops the process.
|
|
168
|
+
const vRemaining = outPolicy.previewBytes - preview.lengths[channel];
|
|
169
|
+
if (vRemaining > 0) {
|
|
170
|
+
const vKept = pKept.subarray(0, Math.min(pKept.length, vRemaining));
|
|
171
|
+
if (vKept.length) { preview[channel].push(vKept); preview.lengths[channel] += vKept.length; }
|
|
172
|
+
if (pKept.length > vRemaining) preview.truncated[channel] = true;
|
|
173
|
+
} else if (chunk.length) preview.truncated[channel] = true;
|
|
174
|
+
if (overcap) { stream.truncated[channel] = true; outputLimitExceeded = true; stop(); }
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
child.stdio[3].on('error', () => { cleanupErrors.add('control_report_read_error'); stop(); });
|
|
178
|
+
child.stdio[3].on('data', chunk => {
|
|
179
|
+
reportBytes += chunk.length;
|
|
180
|
+
if (reportBytes <= 4096) report.push(chunk); else { cleanupErrors.add('control_report_oversize'); stop(); }
|
|
181
|
+
});
|
|
182
|
+
child.stdio[4].on('error', () => { cleanupErrors.add('owner_lease_error'); stop(); });
|
|
183
|
+
const abort = () => { canceled = true; stop(); };
|
|
184
|
+
signal?.addEventListener('abort', abort, { once: true }); if (signal?.aborted) abort();
|
|
185
|
+
const deadline = () => {
|
|
186
|
+
const remaining = duration - (performance.now() - started);
|
|
187
|
+
if (remaining <= 0) { timedOut = true; stop(); }
|
|
188
|
+
else timer = setTimeout(deadline, Math.min(2_147_483_647, Math.max(1, remaining)));
|
|
189
|
+
};
|
|
190
|
+
deadline();
|
|
191
|
+
const groupSignal = signal => {
|
|
192
|
+
if (!child.pid) return;
|
|
193
|
+
try { process.kill(-child.pid, signal); }
|
|
194
|
+
catch (error) { if (error.code !== 'ESRCH') cleanupErrors.add(`group_${signal}:${error.code ?? 'error'}`); }
|
|
195
|
+
};
|
|
196
|
+
try {
|
|
197
|
+
await stopping; clearTimeout(timer); signal?.removeEventListener('abort', abort);
|
|
198
|
+
if (child.pid) {
|
|
199
|
+
groupSignal('SIGTERM'); await wait(200); groupSignal('SIGKILL');
|
|
200
|
+
if (!exited) await boundedWait(exit, 1000);
|
|
201
|
+
if (!exited && !spawnError) { cleanupErrors.add('process_reap_timeout'); try { child.kill('SIGKILL'); } catch { /* recorded below */ } }
|
|
202
|
+
}
|
|
203
|
+
// The only owner writer closes after the last group signal. The watchdog
|
|
204
|
+
// also handles hard caller death, when no finally block can execute.
|
|
205
|
+
child.stdio[4].destroy();
|
|
206
|
+
if (!closed) await boundedWait(close, 200);
|
|
207
|
+
if (!closed && !spawnError) cleanupErrors.add('process_stdio_close_timeout');
|
|
208
|
+
} finally {
|
|
209
|
+
clearTimeout(timer); signal?.removeEventListener('abort', abort);
|
|
210
|
+
for (const stream of child.stdio) stream?.destroy();
|
|
211
|
+
if (!exited) child.unref();
|
|
212
|
+
}
|
|
213
|
+
if (!persistedStream) {
|
|
214
|
+
for (const channel of ['stdout', 'stderr']) {
|
|
215
|
+
const tail = decoders[channel].end();
|
|
216
|
+
if (tail) { try { onOutput?.(channel, tail); } catch { /* observation never changes execution */ } }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (persistedStream) for (const channel of ['stdout', 'stderr']) {
|
|
220
|
+
// Do not synthesize a replacement for a sequence cut by the hard cap.
|
|
221
|
+
// Natural invalid tails are normalized within the same encoded-byte bound.
|
|
222
|
+
if (!stream.truncated[channel]) emitPersisted(channel, stream.decoders[channel].end());
|
|
223
|
+
}
|
|
224
|
+
if (spawnError) throw new LocalProcessError(`local_process_spawn_failed:${spawnError.code ?? 'error'}`);
|
|
225
|
+
const controls = controlReport(Buffer.concat(report).toString('utf8'), settings);
|
|
226
|
+
const code = exitCode ?? (exitSignal && osConstants.signals[exitSignal] ? -osConstants.signals[exitSignal] : null);
|
|
227
|
+
if (!persistedStream)
|
|
228
|
+
return { stdout: Buffer.concat(output.stdout).toString('utf8'), stderr: Buffer.concat(output.stderr).toString('utf8'),
|
|
229
|
+
exitCode: outputLimitExceeded ? 122 : code, timedOut, canceled, outputLimitExceeded,
|
|
230
|
+
stdoutTruncated: truncated.stdout, stderrTruncated: truncated.stderr, cleanupError: [...cleanupErrors].join(',') || null, controls };
|
|
231
|
+
// persisted-stream/v1: stdout/stderr hold ONLY the bounded preview. The full
|
|
232
|
+
// capped output lives in the supervisor-owned files (streamed via onOutput and
|
|
233
|
+
// located by task_id/cursors). Honest truncation flags distinguish preview vs
|
|
234
|
+
// hard persisted bounds; outputPersistedTruncated marks the real boundary.
|
|
235
|
+
const previewText = channel => {
|
|
236
|
+
const decoded = Buffer.concat(preview[channel]).toString('utf8'), bounded = utf8Prefix(decoded, outPolicy.previewBytes);
|
|
237
|
+
if (bounded !== decoded) preview.truncated[channel] = true;
|
|
238
|
+
return bounded;
|
|
239
|
+
};
|
|
240
|
+
return { stdout: previewText('stdout'), stderr: previewText('stderr'),
|
|
241
|
+
exitCode: outputLimitExceeded ? 122 : code, timedOut, canceled, outputLimitExceeded,
|
|
242
|
+
stdoutTruncated: preview.truncated.stdout, stderrTruncated: preview.truncated.stderr,
|
|
243
|
+
stdoutPersistedTruncated: stream.truncated.stdout, stderrPersistedTruncated: stream.truncated.stderr,
|
|
244
|
+
previewBytes: outPolicy.previewBytes, persistedBytes: outPolicy.persistedBytes,
|
|
245
|
+
cleanupError: [...cleanupErrors].join(',') || null, controls };
|
|
246
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { localControlSettings } from './local-process.mjs';
|
|
2
|
+
|
|
3
|
+
export const LOCAL_RESOURCE_PROFILE = 'agentkit.local-resource-profile/v1';
|
|
4
|
+
const profiles = Object.freeze({
|
|
5
|
+
'bounded/v1': Object.freeze({}),
|
|
6
|
+
// Repository packs and build artifacts routinely exceed the restricted
|
|
7
|
+
// per-file allowance. Keep a finite development allowance; explicit Host
|
|
8
|
+
// settings always win. Other limits retain their existing values.
|
|
9
|
+
'local-development/v1': Object.freeze({ maxFileBytes: 1024 ** 3 }),
|
|
10
|
+
});
|
|
11
|
+
export function validLocalResourceProfile(profile) {
|
|
12
|
+
return typeof profile === 'string' && Object.hasOwn(profiles, profile);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Host admission selects this policy, never a model/tool argument. */
|
|
16
|
+
export function localResourceSettings({ profile = 'bounded/v1', settings = {} } = {}) {
|
|
17
|
+
if (!validLocalResourceProfile(profile)) throw Error('invalid_local_resource_profile');
|
|
18
|
+
// Validate raw settings before merging: unknown keys/invalid overrides must
|
|
19
|
+
// not be hidden by profile defaults.
|
|
20
|
+
localControlSettings(settings);
|
|
21
|
+
return localControlSettings({ ...profiles[profile], ...settings });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** These are requested limits. Per-command receipts remain authoritative for
|
|
25
|
+
* OS hard-limit clamping, unsupported controls and actual process outcomes. */
|
|
26
|
+
export function localResourceFacts(profile, settings) {
|
|
27
|
+
if (!validLocalResourceProfile(profile)) throw Error('invalid_local_resource_profile');
|
|
28
|
+
const selected = localControlSettings(settings);
|
|
29
|
+
return { contract: LOCAL_RESOURCE_PROFILE, profile, requestedLimits: Object.fromEntries(
|
|
30
|
+
['cpuSeconds', 'addressSpaceBytes', 'maxProcesses', 'maxOpenFiles', 'maxFileBytes', 'wallSeconds']
|
|
31
|
+
.map(key => [key, selected[key]])) };
|
|
32
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonical, invariant } from '@kingsoftcloud/agentkit-harness-core';
|
|
3
|
+
|
|
4
|
+
// KsADK memory/extraction.py native_user_memory_candidate, reference 355059d.
|
|
5
|
+
// Only whole original human statements enter this path. Transcript extraction
|
|
6
|
+
// and model/tool assertions intentionally have no candidate authority here.
|
|
7
|
+
export const NATIVE_USER_EXTRACTION_POLICY = 'native-original-user-profile/v2';
|
|
8
|
+
export const NATIVE_MEMORY_CANDIDATE = 'agentkit.native-memory-candidate/v1';
|
|
9
|
+
export const memoryDigest = value => {
|
|
10
|
+
// UTF-8 replacement of a lone UTF-16 surrogate would alias a different
|
|
11
|
+
// original input. Python's strict UTF-8 encoder rejects it as well.
|
|
12
|
+
invariant(typeof value === 'string' && value.isWellFormed(), 'native_memory_text_invalid_unicode');
|
|
13
|
+
return 'sha256:' + createHash('sha256').update(value).digest('hex');
|
|
14
|
+
};
|
|
15
|
+
const identify = value => memoryDigest(canonical(value)).slice(7);
|
|
16
|
+
const explicitPatterns = [/记住[::]?\s*(.+)/iu, /别忘了[::]?\s*(.+)/iu,
|
|
17
|
+
/remember\s+(?:that\s+)?(.+)/iu, /请记[::]?\s*(.+)/iu];
|
|
18
|
+
const sessionRetention = /\b(?:for|during|within|in)\s+(?:only\s+)?(?:this|the\s+current)\s+(?:conversation|chat|session|turn)\b|(?:本次|这次|此次|当前|这个|本)(?:会话|对话|聊天)|(?:本轮|这轮)/iu;
|
|
19
|
+
const correction = /(?<prefix>(?:我|本人)?喜欢(?<action>吃|喝|用|看|听|玩))(?:的)?(?:是)?\s*(?<new>.+?)\s*(?:,|,)?\s*(?:而)?不是\s*(?<old>.+?)(?:[。.!!]|$)/iu;
|
|
20
|
+
const generalCorrection = /(?:以后|今后)?(?<subject>[一-龥A-Za-z0-9]{1,12})?(?:请)?(?:不要|别)(?<old>.{1,20}?)[,,]?(?:请)?(?:改成|改为|换成|改用)(?<new>.+?)(?:[。.!!]|$)/iu;
|
|
21
|
+
const hobby = /(?:我|本人)?的?爱好(?<correction>其实|现在|改)?(?:是|改成|变成)\s*(?<value>.+?)(?:[。.!!]|$)/iu;
|
|
22
|
+
const implicitPatterns = [/(?:我|本人)?的?偏好(?:是|为)\s*(.+?)(?:[。.!!]|$)/iu,
|
|
23
|
+
/(?:我|本人)?(?:平时)?(?:喜欢|习惯)(吃|喝|用|看|听|玩)\s*(.+?)(?:[。.!!]|$)/iu];
|
|
24
|
+
const full = (pattern, text) => { const match = pattern.exec(text); return match?.index === 0 && match[0].length === text.length ? match : null; };
|
|
25
|
+
const edgePunctuation = value => value.trim().replace(/^[。.,,]+|[。.,,]+$/gu, '');
|
|
26
|
+
const correctedContent = value => value.trim().replace(/^[。.,, ]+|[。.,, ]+$/gu, '');
|
|
27
|
+
const sessionOnly = text => sessionRetention.test(text.trim().split(/[。!?!?\r\n]|\.(?=\s|$)/u, 1)[0]);
|
|
28
|
+
|
|
29
|
+
export function deriveMemoryProfileSlotKey(content) {
|
|
30
|
+
if (hobby.test(content)) return 'profile.preference.hobby';
|
|
31
|
+
const action = /(?:我|本人)?喜欢(?<action>吃|喝|用|看|听|玩)/iu.exec(content)?.groups.action;
|
|
32
|
+
return action ? 'profile.preference.' + ({ 吃: 'food', 喝: 'drink', 用: 'tool', 看: 'viewing', 听: 'listening', 玩: 'activity' })[action] : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isExplicitMemoryInstruction(value) {
|
|
36
|
+
const text = value.trim();
|
|
37
|
+
if (sessionOnly(text)) return false;
|
|
38
|
+
return explicitPatterns.some(pattern => full(pattern, text)) || !!full(correction, text)
|
|
39
|
+
|| !!full(generalCorrection, text) || !!full(hobby, text)?.groups.correction;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Stable candidate identity is versioned; it need not reproduce Python JSON bytes. */
|
|
43
|
+
export function nativeUserMemoryCandidate({ text, sourceEventId, scope, scopeId }) {
|
|
44
|
+
invariant(typeof text === 'string' && text.isWellFormed() && typeof sourceEventId === 'string' && sourceEventId.isWellFormed()
|
|
45
|
+
&& ['user', 'agent', 'workspace', 'org'].includes(scope) && typeof scopeId === 'string', 'native_memory_candidate_invalid');
|
|
46
|
+
text = text.trim();
|
|
47
|
+
const explicit = isExplicitMemoryInstruction(text), hobbyMatch = full(hobby, text);
|
|
48
|
+
if (!sourceEventId.trim() || !(explicit || implicitPatterns.some(pattern => full(pattern, text)) || hobbyMatch)) return null;
|
|
49
|
+
let content, operation = 'add', reason, importance;
|
|
50
|
+
const changedHobby = hobby.exec(text), general = generalCorrection.exec(text), preference = correction.exec(text);
|
|
51
|
+
// Preserve the reference's implicit hobby fallback after its legacy extractor
|
|
52
|
+
// declined a session-scoped statement. This is still only an observation.
|
|
53
|
+
if (sessionOnly(text)) {
|
|
54
|
+
if (!hobbyMatch || hobbyMatch.groups.correction) return null;
|
|
55
|
+
content = '我的爱好是' + hobbyMatch.groups.value.trim(); reason = 'implicit_user_preference'; importance = .65;
|
|
56
|
+
} else if (changedHobby?.groups.correction) {
|
|
57
|
+
const value = correctedContent(changedHobby.groups.value);
|
|
58
|
+
if (!value) return null;
|
|
59
|
+
content = '我的爱好是' + value; operation = 'update'; reason = 'explicit_user_correction'; importance = .9;
|
|
60
|
+
} else if (general) {
|
|
61
|
+
const subject = (general.groups.subject ?? '').trim(), value = correctedContent(general.groups.new);
|
|
62
|
+
if (!value) return null;
|
|
63
|
+
content = subject + '改为' + value; operation = 'update'; reason = 'explicit_user_correction'; importance = .8;
|
|
64
|
+
} else if (preference) {
|
|
65
|
+
const value = correctedContent(preference.groups.new);
|
|
66
|
+
if (!value) return null;
|
|
67
|
+
content = preference.groups.prefix.trim() + value; operation = 'update'; reason = 'explicit_user_correction'; importance = .9;
|
|
68
|
+
} else {
|
|
69
|
+
const instruction = explicitPatterns.map(pattern => pattern.exec(text)).find(Boolean);
|
|
70
|
+
if (instruction) {
|
|
71
|
+
content = edgePunctuation(instruction[1] || text);
|
|
72
|
+
if (!content) return null;
|
|
73
|
+
reason = 'explicit_user_request'; importance = .8;
|
|
74
|
+
} else {
|
|
75
|
+
const implicit = implicitPatterns.map(pattern => pattern.exec(text)).find(Boolean);
|
|
76
|
+
if (implicit) content = edgePunctuation(implicit[0]);
|
|
77
|
+
else if (hobbyMatch && !hobbyMatch.groups.correction) content = '我的爱好是' + hobbyMatch.groups.value.trim();
|
|
78
|
+
else return null;
|
|
79
|
+
reason = 'implicit_user_preference'; importance = .65;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!explicit && reason !== 'implicit_user_preference') return null;
|
|
83
|
+
return { contract: NATIVE_MEMORY_CANDIDATE, extractionPolicy: NATIVE_USER_EXTRACTION_POLICY,
|
|
84
|
+
candidateId: 'candidate_' + identify([NATIVE_MEMORY_CANDIDATE, NATIVE_USER_EXTRACTION_POLICY, scope, scopeId, sourceEventId, operation, content]),
|
|
85
|
+
operation, memoryType: 'profile', scope, scopeId, content, confidence: 1, importance,
|
|
86
|
+
sourceEventIds: [sourceEventId], conflictsWith: [], sensitiveLabels: [], reason,
|
|
87
|
+
slotKey: deriveMemoryProfileSlotKey(content), sensitivity: 'none', expiresAt: '' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function nativeMemoryScopeBindings(principal, scopes = ['user']) {
|
|
91
|
+
invariant(principal && ['tenantId', 'workspaceId', 'agentId', 'userId'].every(key => typeof principal[key] === 'string'
|
|
92
|
+
&& principal[key].trim()), 'native_memory_principal_invalid');
|
|
93
|
+
const { tenantId, workspaceId, agentId, userId } = principal;
|
|
94
|
+
const bindings = { tenant: ['org', [tenantId]], workspace: ['workspace', [tenantId, workspaceId]],
|
|
95
|
+
agent: ['agent', [tenantId, workspaceId, agentId]], user: ['user', [tenantId, workspaceId, agentId, userId]] };
|
|
96
|
+
invariant(Array.isArray(scopes) && scopes.every(scope => Object.hasOwn(bindings, scope)), 'native_memory_scope_invalid');
|
|
97
|
+
return [...new Set(scopes)].map(name => ({ scope: bindings[name][0],
|
|
98
|
+
scopeId: 'native-memory-v1:' + identify([name, ...bindings[name][1]]) }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const secretPatterns = [[/api[_-]?key\s*[:=]\s*\S+/iu, 'api_key'], [/secret[_-]?key\s*[:=]\s*\S+/iu, 'secret_key'],
|
|
102
|
+
[/access[_-]?key\s*[:=]\s*\S+|AKIA[0-9A-Z]{16}/iu, 'access_key'], [/cookie\s*[:=]\s*\S+/iu, 'cookie'],
|
|
103
|
+
[/authorization\s*[:=]\s*bearer\s+\S+/iu, 'auth_header'], [/https?:\/\/\S+?(?:X-Amz-Signature|X-Amz-Security-Token|signed)=/iu, 'signed_url'],
|
|
104
|
+
[/(postgres|mysql|mongodb|redis):\/\/\S+:\S+@\S+/iu, 'dsn'], [/sk-[A-Za-z0-9]{20,}/iu, 'token']];
|
|
105
|
+
export function nativeMemorySensitiveLabels(content, supplied = []) {
|
|
106
|
+
return [...new Set([...supplied.filter(label => label && label !== 'none'),
|
|
107
|
+
...secretPatterns.filter(([pattern]) => pattern.test(content)).map(([, label]) => label)])].sort();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Candidate decisions grant no execution authority; provider commits separately. */
|
|
111
|
+
export function evaluateNativeMemoryCandidate(candidate, { existing = null, observations = 1 } = {}) {
|
|
112
|
+
const labels = nativeMemorySensitiveLabels(candidate.content, candidate.sensitiveLabels);
|
|
113
|
+
if (labels.length || candidate.sensitivity === 'high') return { decision: 'reject', operation: 'ignore', reason: 'sensitive_label_rejected:' + (labels.join(',') || 'explicit') };
|
|
114
|
+
if (/model_guess|transient|current_plan/iu.test(candidate.reason)) return { decision: 'reject', operation: 'ignore', reason: 'not_durable' };
|
|
115
|
+
const explicit = /explicit|user_request/iu.test(candidate.reason);
|
|
116
|
+
const threshold = explicit ? .6 : observations < 2 ? Infinity : .85;
|
|
117
|
+
if (candidate.confidence < threshold) return { decision: 'pending', operation: candidate.operation,
|
|
118
|
+
reason: `below_threshold:${candidate.confidence.toFixed(2)}<${Number.isFinite(threshold) ? threshold.toFixed(2) : 'inf'}` };
|
|
119
|
+
return { decision: 'commit', operation: existing ? 'update' : candidate.operation,
|
|
120
|
+
reason: existing ? 'conflict_supersede' : 'threshold_met' };
|
|
121
|
+
}
|